flutter_dashboard/lib/screens/wakatime_screen.dart
jdysya 569b4f4498 feat(wakatime): 添加 Wakatime 平台支持
- 在首页添加 Wakatime 卡片和相关功能
- 在设置页面添加 Wakatime API Token 配置
- 更新 auth_service 以支持 Wakatime 认证
- 添加 fl_chart 依赖用于 Wakatime 数据图表展示
- 更新 flutter_lints 版本到 2.0.0
2025-06-10 12:07:33 +08:00

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();
}
}