diff --git a/lib/models/wakatime_summary.dart b/lib/models/wakatime_summary.dart new file mode 100644 index 0000000..37959d8 --- /dev/null +++ b/lib/models/wakatime_summary.dart @@ -0,0 +1,541 @@ +class WakatimeSummary { + final List data; + final String start; + final String end; + final CumulativeTotal cumulativeTotal; + final DailyAverage dailyAverage; + + WakatimeSummary({ + required this.data, + required this.start, + required this.end, + required this.cumulativeTotal, + required this.dailyAverage, + }); + + factory WakatimeSummary.fromJson(Map json) { + return WakatimeSummary( + data: (json['data'] as List).map((e) => SummaryData.fromJson(e)).toList(), + start: json['start'], + end: json['end'], + cumulativeTotal: CumulativeTotal.fromJson(json['cumulative_total']), + dailyAverage: DailyAverage.fromJson(json['daily_average']), + ); + } + + // 汇总语言数据 + List getAggregatedLanguages() { + final Map aggregated = {}; + + for (var dayData in data) { + for (var lang in dayData.languages) { + if (aggregated.containsKey(lang.name)) { + final existing = aggregated[lang.name]!; + aggregated[lang.name] = Language( + name: lang.name, + totalSeconds: existing.totalSeconds + lang.totalSeconds, + digital: _formatTime(existing.totalSeconds + lang.totalSeconds), + decimal: ((existing.totalSeconds + lang.totalSeconds) / 3600) + .toStringAsFixed(2), + text: _formatTimeText(existing.totalSeconds + lang.totalSeconds), + hours: ((existing.totalSeconds + lang.totalSeconds) / 3600).floor(), + minutes: ((existing.totalSeconds + lang.totalSeconds) % 3600 / 60) + .floor(), + seconds: ((existing.totalSeconds + lang.totalSeconds) % 60).floor(), + percent: 0, // 将在计算完总和后更新 + ); + } else { + aggregated[lang.name] = lang; + } + } + } + + final totalSeconds = aggregated.values + .fold(0, (sum, lang) => sum + lang.totalSeconds); + final result = aggregated.values + .map((lang) => Language( + name: lang.name, + totalSeconds: lang.totalSeconds, + digital: lang.digital, + decimal: lang.decimal, + text: lang.text, + hours: lang.hours, + minutes: lang.minutes, + seconds: lang.seconds, + percent: (lang.totalSeconds / totalSeconds * 100), + )) + .toList(); + + // 按使用时间排序 + result.sort((a, b) => b.totalSeconds.compareTo(a.totalSeconds)); + return result; + } + + // 汇总编辑器数据 + List getAggregatedEditors() { + final Map aggregated = {}; + + for (var dayData in data) { + for (var editor in dayData.editors) { + if (aggregated.containsKey(editor.name)) { + final existing = aggregated[editor.name]!; + aggregated[editor.name] = Editor( + name: editor.name, + totalSeconds: existing.totalSeconds + editor.totalSeconds, + digital: _formatTime(existing.totalSeconds + editor.totalSeconds), + decimal: ((existing.totalSeconds + editor.totalSeconds) / 3600) + .toStringAsFixed(2), + text: _formatTimeText(existing.totalSeconds + editor.totalSeconds), + hours: + ((existing.totalSeconds + editor.totalSeconds) / 3600).floor(), + minutes: ((existing.totalSeconds + editor.totalSeconds) % 3600 / 60) + .floor(), + seconds: + ((existing.totalSeconds + editor.totalSeconds) % 60).floor(), + percent: 0, // 将在计算完总和后更新 + ); + } else { + aggregated[editor.name] = editor; + } + } + } + + final totalSeconds = aggregated.values + .fold(0, (sum, editor) => sum + editor.totalSeconds); + final result = aggregated.values + .map((editor) => Editor( + name: editor.name, + totalSeconds: editor.totalSeconds, + digital: editor.digital, + decimal: editor.decimal, + text: editor.text, + hours: editor.hours, + minutes: editor.minutes, + seconds: editor.seconds, + percent: (editor.totalSeconds / totalSeconds * 100), + )) + .toList(); + + // 按使用时间排序 + result.sort((a, b) => b.totalSeconds.compareTo(a.totalSeconds)); + return result; + } + + // 汇总项目数据 + List getAggregatedProjects() { + final Map aggregated = {}; + + for (var dayData in data) { + for (var project in dayData.projects) { + if (aggregated.containsKey(project.name)) { + final existing = aggregated[project.name]!; + aggregated[project.name] = Project( + name: project.name, + totalSeconds: existing.totalSeconds + project.totalSeconds, + color: project.color, + digital: _formatTime(existing.totalSeconds + project.totalSeconds), + decimal: ((existing.totalSeconds + project.totalSeconds) / 3600) + .toStringAsFixed(2), + text: _formatTimeText(existing.totalSeconds + project.totalSeconds), + hours: + ((existing.totalSeconds + project.totalSeconds) / 3600).floor(), + minutes: + ((existing.totalSeconds + project.totalSeconds) % 3600 / 60) + .floor(), + seconds: + ((existing.totalSeconds + project.totalSeconds) % 60).floor(), + percent: 0, // 将在计算完总和后更新 + ); + } else { + aggregated[project.name] = project; + } + } + } + + final totalSeconds = aggregated.values + .fold(0, (sum, project) => sum + project.totalSeconds); + final result = aggregated.values + .map((project) => Project( + name: project.name, + totalSeconds: project.totalSeconds, + color: project.color, + digital: project.digital, + decimal: project.decimal, + text: project.text, + hours: project.hours, + minutes: project.minutes, + seconds: project.seconds, + percent: (project.totalSeconds / totalSeconds * 100), + )) + .toList(); + + // 按使用时间排序 + result.sort((a, b) => b.totalSeconds.compareTo(a.totalSeconds)); + return result; + } + + // 格式化时间为 HH:mm:ss + String _formatTime(double seconds) { + final hours = (seconds / 3600).floor(); + final minutes = ((seconds % 3600) / 60).floor(); + final secs = (seconds % 60).floor(); + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + + // 格式化时间为可读文本 + String _formatTimeText(double seconds) { + final hours = (seconds / 3600).floor(); + final minutes = ((seconds % 3600) / 60).floor(); + final secs = (seconds % 60).floor(); + + if (hours > 0) { + return '$hours hr${hours > 1 ? 's' : ''} ${minutes > 0 ? '$minutes min${minutes > 1 ? 's' : ''}' : ''}'; + } else if (minutes > 0) { + return '$minutes min${minutes > 1 ? 's' : ''}'; + } else { + return '$secs sec${secs > 1 ? 's' : ''}'; + } + } +} + +class SummaryData { + final GrandTotal grandTotal; + final Range range; + final List projects; + final List languages; + final List editors; + final List operatingSystems; + final List categories; + + SummaryData({ + required this.grandTotal, + required this.range, + required this.projects, + required this.languages, + required this.editors, + required this.operatingSystems, + required this.categories, + }); + + factory SummaryData.fromJson(Map json) { + return SummaryData( + grandTotal: GrandTotal.fromJson(json['grand_total']), + range: Range.fromJson(json['range']), + projects: + (json['projects'] as List).map((e) => Project.fromJson(e)).toList(), + languages: + (json['languages'] as List).map((e) => Language.fromJson(e)).toList(), + editors: + (json['editors'] as List).map((e) => Editor.fromJson(e)).toList(), + operatingSystems: (json['operating_systems'] as List) + .map((e) => OperatingSystem.fromJson(e)) + .toList(), + categories: (json['categories'] as List) + .map((e) => Category.fromJson(e)) + .toList(), + ); + } +} + +class GrandTotal { + final int hours; + final int minutes; + final double totalSeconds; + final String digital; + final String decimal; + final String text; + + GrandTotal({ + required this.hours, + required this.minutes, + required this.totalSeconds, + required this.digital, + required this.decimal, + required this.text, + }); + + factory GrandTotal.fromJson(Map json) { + return GrandTotal( + hours: json['hours'], + minutes: json['minutes'], + totalSeconds: json['total_seconds'].toDouble(), + digital: json['digital'], + decimal: json['decimal'], + text: json['text'], + ); + } +} + +class Range { + final String start; + final String end; + final String date; + final String text; + final String timezone; + + Range({ + required this.start, + required this.end, + required this.date, + required this.text, + required this.timezone, + }); + + factory Range.fromJson(Map json) { + return Range( + start: json['start'], + end: json['end'], + date: json['date'], + text: json['text'], + timezone: json['timezone'], + ); + } +} + +class Project { + final String name; + final double totalSeconds; + final String? color; + final String digital; + final String decimal; + final String text; + final int hours; + final int minutes; + final int seconds; + final double percent; + + Project({ + required this.name, + required this.totalSeconds, + this.color, + required this.digital, + required this.decimal, + required this.text, + required this.hours, + required this.minutes, + required this.seconds, + required this.percent, + }); + + factory Project.fromJson(Map json) { + return Project( + name: json['name'], + totalSeconds: json['total_seconds'].toDouble(), + color: json['color'], + digital: json['digital'], + decimal: json['decimal'], + text: json['text'], + hours: json['hours'], + minutes: json['minutes'], + seconds: json['seconds'], + percent: json['percent'].toDouble(), + ); + } +} + +class Language { + final String name; + final double totalSeconds; + final String digital; + final String decimal; + final String text; + final int hours; + final int minutes; + final int seconds; + final double percent; + + Language({ + required this.name, + required this.totalSeconds, + required this.digital, + required this.decimal, + required this.text, + required this.hours, + required this.minutes, + required this.seconds, + required this.percent, + }); + + factory Language.fromJson(Map json) { + return Language( + name: json['name'], + totalSeconds: json['total_seconds'].toDouble(), + digital: json['digital'], + decimal: json['decimal'], + text: json['text'], + hours: json['hours'], + minutes: json['minutes'], + seconds: json['seconds'], + percent: json['percent'].toDouble(), + ); + } +} + +class Editor { + final String name; + final double totalSeconds; + final String digital; + final String decimal; + final String text; + final int hours; + final int minutes; + final int seconds; + final double percent; + + Editor({ + required this.name, + required this.totalSeconds, + required this.digital, + required this.decimal, + required this.text, + required this.hours, + required this.minutes, + required this.seconds, + required this.percent, + }); + + factory Editor.fromJson(Map json) { + return Editor( + name: json['name'], + totalSeconds: json['total_seconds'].toDouble(), + digital: json['digital'], + decimal: json['decimal'], + text: json['text'], + hours: json['hours'], + minutes: json['minutes'], + seconds: json['seconds'], + percent: json['percent'].toDouble(), + ); + } +} + +class OperatingSystem { + final String name; + final double totalSeconds; + final String digital; + final String decimal; + final String text; + final int hours; + final int minutes; + final int seconds; + final double percent; + + OperatingSystem({ + required this.name, + required this.totalSeconds, + required this.digital, + required this.decimal, + required this.text, + required this.hours, + required this.minutes, + required this.seconds, + required this.percent, + }); + + factory OperatingSystem.fromJson(Map json) { + return OperatingSystem( + name: json['name'], + totalSeconds: json['total_seconds'].toDouble(), + digital: json['digital'], + decimal: json['decimal'], + text: json['text'], + hours: json['hours'], + minutes: json['minutes'], + seconds: json['seconds'], + percent: json['percent'].toDouble(), + ); + } +} + +class Category { + final String name; + final double totalSeconds; + final String digital; + final String decimal; + final String text; + final int hours; + final int minutes; + final int seconds; + final double percent; + + Category({ + required this.name, + required this.totalSeconds, + required this.digital, + required this.decimal, + required this.text, + required this.hours, + required this.minutes, + required this.seconds, + required this.percent, + }); + + factory Category.fromJson(Map json) { + return Category( + name: json['name'], + totalSeconds: json['total_seconds'].toDouble(), + digital: json['digital'], + decimal: json['decimal'], + text: json['text'], + hours: json['hours'], + minutes: json['minutes'], + seconds: json['seconds'], + percent: json['percent'].toDouble(), + ); + } +} + +class CumulativeTotal { + final double seconds; + final String text; + final String digital; + final String decimal; + + CumulativeTotal({ + required this.seconds, + required this.text, + required this.digital, + required this.decimal, + }); + + factory CumulativeTotal.fromJson(Map json) { + return CumulativeTotal( + seconds: json['seconds'].toDouble(), + text: json['text'], + digital: json['digital'], + decimal: json['decimal'], + ); + } +} + +class DailyAverage { + final int holidays; + final int daysMinusHolidays; + final int daysIncludingHolidays; + final int seconds; + final int secondsIncludingOtherLanguage; + final String text; + final String textIncludingOtherLanguage; + + DailyAverage({ + required this.holidays, + required this.daysMinusHolidays, + required this.daysIncludingHolidays, + required this.seconds, + required this.secondsIncludingOtherLanguage, + required this.text, + required this.textIncludingOtherLanguage, + }); + + factory DailyAverage.fromJson(Map json) { + return DailyAverage( + holidays: json['holidays'], + daysMinusHolidays: json['days_minus_holidays'], + daysIncludingHolidays: json['days_including_holidays'], + seconds: json['seconds'], + secondsIncludingOtherLanguage: json['seconds_including_other_language'], + text: json['text'], + textIncludingOtherLanguage: json['text_including_other_language'], + ); + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 8ed81d7..1b0c97b 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -6,6 +6,7 @@ import '../widgets/gitea_card.dart'; import '../widgets/kodbox_card.dart'; import '../widgets/github_card.dart'; import 'settings_screen.dart'; +import 'wakatime_screen.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -16,7 +17,13 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { int _selectedIndex = 0; - final List _platforms = ['LeetCode', 'Gitea', 'KodBox', 'GitHub']; + final List _platforms = [ + 'LeetCode', + 'Gitea', + 'KodBox', + 'GitHub', + 'Wakatime' + ]; @override void initState() { @@ -82,6 +89,8 @@ class _HomeScreenState extends State { return Icons.folder; case 'GitHub': return Icons.code; + case 'Wakatime': + return Icons.timer; default: return Icons.help_outline; } @@ -97,6 +106,8 @@ class _HomeScreenState extends State { return const KodBoxCard(); case 'GitHub': return const GithubCard(); + case 'Wakatime': + return const WakatimeScreen(); default: return const Center(child: Text('未知平台')); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f4e514c..7202509 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -18,6 +18,7 @@ class _SettingsScreenState extends State { final _kodboxController = TextEditingController(); final _githubUsernameController = TextEditingController(); final _githubTokenController = TextEditingController(); + final _wakatimeTokenController = TextEditingController(); @override void initState() { @@ -34,6 +35,7 @@ class _SettingsScreenState extends State { _kodboxController.dispose(); _githubUsernameController.dispose(); _githubTokenController.dispose(); + _wakatimeTokenController.dispose(); super.dispose(); } @@ -54,6 +56,8 @@ class _SettingsScreenState extends State { authService.credentials['github_username'] ?? ''; _githubTokenController.text = authService.credentials['github_token'] ?? ''; + _wakatimeTokenController.text = + authService.credentials['wakatime_token'] ?? ''; }); } @@ -97,6 +101,12 @@ class _SettingsScreenState extends State { _githubTokenController.text, ); } + if (_wakatimeTokenController.text.isNotEmpty) { + await authService.saveConfigs( + 'wakatime_token', + _wakatimeTokenController.text, + ); + } if (mounted) { ScaffoldMessenger.of( @@ -206,6 +216,22 @@ class _SettingsScreenState extends State { ), ], ), + const SizedBox(height: 16), + _buildPlatformSettings( + title: 'Wakatime 设置', + icon: Icons.timer, + children: [ + TextFormField( + controller: _wakatimeTokenController, + decoration: const InputDecoration( + labelText: 'API Token', + hintText: '请输入 Wakatime API Token', + border: OutlineInputBorder(), + ), + obscureText: true, + ), + ], + ), const SizedBox(height: 24), ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')), ], diff --git a/lib/screens/wakatime_screen.dart b/lib/screens/wakatime_screen.dart new file mode 100644 index 0000000..022263d --- /dev/null +++ b/lib/screens/wakatime_screen.dart @@ -0,0 +1,337 @@ +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 { + final WakatimeService _wakatimeService = WakatimeService(); + WakatimeSummary? _summary; + bool _isLoading = false; + String? _error; + final TextEditingController _startDateController = TextEditingController(); + final TextEditingController _endDateController = TextEditingController(); + int? _touchedIndex; + + // 预定义的颜色列表 + 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(); + // 设置默认日期为今天和昨天 + 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 _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; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + Widget _buildLegend(List 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 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(); + } +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 72e835c..9d48207 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -24,6 +24,7 @@ class AuthService extends ChangeNotifier { final kodboxToken = await _storage.read(key: 'kodbox_token'); final githubUserName = await _storage.read(key: 'github_username'); final githubToken = await _storage.read(key: 'github_token'); + final wakatimeToken = await _storage.read(key: 'wakatime_token'); if (leetcodeCookie != null) { _credentials['leetcode_cookie'] = leetcodeCookie; @@ -46,6 +47,9 @@ class AuthService extends ChangeNotifier { if (githubToken != null) { _credentials['github_token'] = githubToken; } + if (wakatimeToken != null) { + _credentials['wakatime_token'] = wakatimeToken; + } _isAuthenticated = true; notifyListeners(); } diff --git a/lib/services/wakatime_service.dart b/lib/services/wakatime_service.dart new file mode 100644 index 0000000..f6484e4 --- /dev/null +++ b/lib/services/wakatime_service.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../models/wakatime_summary.dart'; + +class WakatimeService { + static const String baseUrl = 'https://wakatime.com/api/v1'; + + Future getSummary( + String token, + String startDate, + String endDate, + ) async { + final url = Uri.parse( + '$baseUrl/users/current/summaries?start=$startDate&end=$endDate', + ); + final response = await http.get( + url, + headers: {'Authorization': 'Basic ${base64Encode(utf8.encode(token))}'}, + ); + + if (response.statusCode == 200) { + return WakatimeSummary.fromJson(json.decode(response.body)); + } else { + throw Exception('Failed to load summary: ${response.statusCode}'); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 510478d..619ee5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.65.0" flutter: dependency: "direct main" description: flutter @@ -130,10 +146,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "5.0.0" + version: "2.0.3" flutter_secure_storage: dependency: "direct main" description: @@ -252,10 +268,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "5.1.1" + version: "2.1.1" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 90a2d67..477f584 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ^3.7.0 + sdk: '>=3.2.3 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -33,14 +33,15 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - http: ^1.2.0 + cupertino_icons: ^1.0.2 + http: ^1.1.0 provider: ^6.1.1 shared_preferences: ^2.2.2 intl: ^0.19.0 flutter_secure_storage: ^9.0.0 cached_network_image: ^3.3.1 timeago: ^3.7.1 + fl_chart: ^0.65.0 dev_dependencies: flutter_test: @@ -51,7 +52,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 + flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec