- 在首页添加 Wakatime 卡片和相关功能 - 在设置页面添加 Wakatime API Token 配置 - 更新 auth_service 以支持 Wakatime 认证 - 添加 fl_chart 依赖用于 Wakatime 数据图表展示 - 更新 flutter_lints 版本到 2.0.0
338 lines
10 KiB
Dart
338 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../models/wakatime_summary.dart';
|
|
import '../services/wakatime_service.dart';
|
|
import '../services/auth_service.dart';
|
|
|
|
class WakatimeScreen extends StatefulWidget {
|
|
const WakatimeScreen({super.key});
|
|
|
|
@override
|
|
State<WakatimeScreen> createState() => _WakatimeScreenState();
|
|
}
|
|
|
|
class _WakatimeScreenState extends State<WakatimeScreen> {
|
|
final WakatimeService _wakatimeService = WakatimeService();
|
|
WakatimeSummary? _summary;
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
final TextEditingController _startDateController = TextEditingController();
|
|
final TextEditingController _endDateController = TextEditingController();
|
|
int? _touchedIndex;
|
|
|
|
// 预定义的颜色列表
|
|
final List<Color> _colors = [
|
|
Colors.blue,
|
|
Colors.red,
|
|
Colors.green,
|
|
Colors.orange,
|
|
Colors.purple,
|
|
Colors.teal,
|
|
Colors.pink,
|
|
Colors.indigo,
|
|
Colors.amber,
|
|
Colors.cyan,
|
|
Colors.lime,
|
|
Colors.brown,
|
|
];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 设置默认日期为今天和昨天
|
|
final now = DateTime.now();
|
|
_startDateController.text =
|
|
_formatDate(now.subtract(const Duration(days: 1)));
|
|
_endDateController.text = _formatDate(now);
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
String _formatDuration(int seconds) {
|
|
final hours = seconds ~/ 3600;
|
|
final minutes = (seconds % 3600) ~/ 60;
|
|
return '${hours}小时${minutes}分钟';
|
|
}
|
|
|
|
Future<void> _fetchData() async {
|
|
final authService = Provider.of<AuthService>(context, listen: false);
|
|
final token = authService.credentials['wakatime_token'];
|
|
|
|
if (token == null || token.isEmpty) {
|
|
setState(() => _error = '请在设置中配置Wakatime API Token');
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
});
|
|
|
|
try {
|
|
final summary = await _wakatimeService.getSummary(
|
|
token,
|
|
_startDateController.text,
|
|
_endDateController.text,
|
|
);
|
|
setState(() {
|
|
_summary = summary;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_error = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget _buildLegend(List<dynamic> data, String Function(dynamic) getName) {
|
|
return Wrap(
|
|
spacing: 16,
|
|
runSpacing: 8,
|
|
children: data.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final item = entry.value;
|
|
final isTouched = index == _touchedIndex;
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 16,
|
|
height: 16,
|
|
color: _colors[index % _colors.length],
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${getName(item)}${isTouched ? ' (${_formatDuration(item.totalSeconds)})' : ''}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: isTouched ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
);
|
|
}
|
|
|
|
Widget _buildPieChart(
|
|
List<dynamic> data, String title, String Function(dynamic) getName) {
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildLegend(data, getName),
|
|
const SizedBox(height: 16),
|
|
SizedBox(
|
|
height: 300,
|
|
child: PieChart(
|
|
PieChartData(
|
|
pieTouchData: PieTouchData(
|
|
touchCallback: (FlTouchEvent event, pieTouchResponse) {
|
|
setState(() {
|
|
if (!event.isInterestedForInteractions ||
|
|
pieTouchResponse == null ||
|
|
pieTouchResponse.touchedSection == null) {
|
|
_touchedIndex = null;
|
|
return;
|
|
}
|
|
_touchedIndex =
|
|
pieTouchResponse.touchedSection!.touchedSectionIndex;
|
|
});
|
|
},
|
|
),
|
|
sections: data.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final item = entry.value;
|
|
final isTouched = index == _touchedIndex;
|
|
final percent = item.percent;
|
|
return PieChartSectionData(
|
|
value: item.totalSeconds.toDouble(),
|
|
title: percent > 5
|
|
? '${getName(item)}\n${percent.toStringAsFixed(1)}%'
|
|
: '',
|
|
radius: isTouched ? 110 : 100,
|
|
titleStyle: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
color: _colors[index % _colors.length],
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildProjectList() {
|
|
final projects = _summary!.getAggregatedProjects();
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'项目时间分布',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: projects.length,
|
|
itemBuilder: (context, index) {
|
|
final project = projects[index];
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 16,
|
|
height: 16,
|
|
color: _colors[index % _colors.length],
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
project.name,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_formatDuration(project.totalSeconds.toInt()),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Text(
|
|
'${project.percent.toStringAsFixed(1)}%',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Wakatime 统计'),
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 日期选择
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _startDateController,
|
|
decoration: const InputDecoration(
|
|
labelText: '开始日期',
|
|
hintText: 'YYYY-MM-DD',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _endDateController,
|
|
decoration: const InputDecoration(
|
|
labelText: '结束日期',
|
|
hintText: 'YYYY-MM-DD',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 获取数据按钮
|
|
ElevatedButton(
|
|
onPressed: _isLoading ? null : _fetchData,
|
|
child: _isLoading
|
|
? const CircularProgressIndicator()
|
|
: const Text('获取数据'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 错误提示
|
|
if (_error != null)
|
|
Text(
|
|
_error!,
|
|
style: const TextStyle(color: Colors.red),
|
|
),
|
|
|
|
// 数据展示
|
|
if (_summary != null) ...[
|
|
const Text(
|
|
'总编码时间',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(_summary!.cumulativeTotal.text),
|
|
const SizedBox(height: 24),
|
|
|
|
// 语言使用饼图
|
|
_buildPieChart(
|
|
_summary!.getAggregatedLanguages(),
|
|
'编程语言使用情况',
|
|
(lang) => lang.name,
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// 编辑器使用饼图
|
|
_buildPieChart(
|
|
_summary!.getAggregatedEditors(),
|
|
'编辑器使用情况',
|
|
(editor) => editor.name,
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// 项目时间分布
|
|
_buildProjectList(),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_startDateController.dispose();
|
|
_endDateController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|