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 createState() => _WakatimeScreenState(); } class _WakatimeScreenState extends State with SingleTickerProviderStateMixin { final WakatimeService _wakatimeService = WakatimeService(); WakatimeSummary? _summary; bool _isLoading = false; String? _error; final TextEditingController _startDateController = TextEditingController(); final TextEditingController _endDateController = TextEditingController(); int? _touchedIndex; String? _touchedChartType; late AnimationController _animationController; late Animation _animation; // 预定义的颜色列表 final List _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(); _animationController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, ); _animation = CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ); _animationController.forward(); // 设置默认日期为今天和一周前 final now = DateTime.now(); _startDateController.text = _formatDate(now.subtract(const Duration(days: 7))); _endDateController.text = _formatDate(now); _fetchData(); } 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 _fetchData() async { final authService = Provider.of(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; }); _animationController.reset(); _animationController.forward(); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } Widget _buildLegend( List data, String Function(dynamic) getName, String chartType) { return Wrap( spacing: 16, runSpacing: 8, children: data.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final isTouched = index == _touchedIndex && _touchedChartType == chartType; 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.toInt())})' : ''}', style: TextStyle( fontSize: 12, fontWeight: isTouched ? FontWeight.bold : FontWeight.normal, ), ), ], ); }).toList(), ); } Widget _buildPieChart(List data, String title, String Function(dynamic) getName, String chartType) { return Column( children: [ Text( title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), _buildLegend(data, getName, chartType), 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; _touchedChartType = null; return; } _touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; _touchedChartType = chartType; }); }, ), sections: data.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final isTouched = index == _touchedIndex && _touchedChartType == chartType; 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, ), ), ], ), ), ); }, ), ], ); } Widget _buildTotalTime() { if (_summary == null) return const SizedBox.shrink(); return FadeTransition( opacity: _animation, child: SlideTransition( position: Tween( begin: const Offset(0, 0.3), end: Offset.zero, ).animate(_animation), child: Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Theme.of(context).primaryColor, Theme.of(context).primaryColor.withOpacity(0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Theme.of(context).primaryColor.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Column( children: [ const Text( '总编码时间', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 16), Text( _summary!.cumulativeTotal.text, style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 8), Text( '${_startDateController.text} 至 ${_endDateController.text}', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.8), ), ), ], ), ), ), ); } @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), // 获取数据按钮 Center( child: 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) ...[ _buildTotalTime(), const SizedBox(height: 24), // 项目时间分布 _buildProjectList(), const SizedBox(height: 24), // 编辑器使用饼图 _buildPieChart( _summary!.getAggregatedEditors(), '编辑器使用情况', (editor) => editor.name, 'editor', ), const SizedBox(height: 24), // 语言使用饼图 _buildPieChart( _summary!.getAggregatedLanguages(), '编程语言使用情况', (lang) => lang.name, 'language', ), ], ], ), ), ); } @override void dispose() { _startDateController.dispose(); _endDateController.dispose(); _animationController.dispose(); super.dispose(); } }