Compare commits

..

No commits in common. "main" and "1.0" have entirely different histories.
main ... 1.0

19 changed files with 98 additions and 1817 deletions

View File

@ -1,6 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<application <application
android:label="dashboard" android:label="dashboard"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -1,541 +0,0 @@
class WakatimeSummary {
final List<SummaryData> 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<String, dynamic> 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<Language> getAggregatedLanguages() {
final Map<String, Language> 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<double>(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<Editor> getAggregatedEditors() {
final Map<String, Editor> 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<double>(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<Project> getAggregatedProjects() {
final Map<String, Project> 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<double>(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<Project> projects;
final List<Language> languages;
final List<Editor> editors;
final List<OperatingSystem> operatingSystems;
final List<Category> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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'],
);
}
}

View File

@ -6,7 +6,6 @@ import '../widgets/gitea_card.dart';
import '../widgets/kodbox_card.dart'; import '../widgets/kodbox_card.dart';
import '../widgets/github_card.dart'; import '../widgets/github_card.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'wakatime_screen.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -17,13 +16,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<String> _platforms = [ final List<String> _platforms = ['LeetCode', 'Gitea', 'KodBox', 'GitHub'];
'Wakatime',
'LeetCode',
'GitHub',
'Gitea',
'KodBox',
];
@override @override
void initState() { void initState() {
@ -89,8 +82,6 @@ class _HomeScreenState extends State<HomeScreen> {
return Icons.folder; return Icons.folder;
case 'GitHub': case 'GitHub':
return Icons.code; return Icons.code;
case 'Wakatime':
return Icons.timer;
default: default:
return Icons.help_outline; return Icons.help_outline;
} }
@ -106,8 +97,6 @@ class _HomeScreenState extends State<HomeScreen> {
return const KodBoxCard(); return const KodBoxCard();
case 'GitHub': case 'GitHub':
return const GithubCard(); return const GithubCard();
case 'Wakatime':
return const WakatimeScreen();
default: default:
return const Center(child: Text('未知平台')); return const Center(child: Text('未知平台'));
} }

View File

@ -15,11 +15,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final _leetcodeUserSlugController = TextEditingController(); final _leetcodeUserSlugController = TextEditingController();
final _giteaController = TextEditingController(); final _giteaController = TextEditingController();
final _giteaUsernameController = TextEditingController(); final _giteaUsernameController = TextEditingController();
final _kodboxUsernameController = TextEditingController(); final _kodboxController = TextEditingController();
final _kodboxPasswordController = TextEditingController();
final _githubUsernameController = TextEditingController(); final _githubUsernameController = TextEditingController();
final _githubTokenController = TextEditingController(); final _githubTokenController = TextEditingController();
final _wakatimeTokenController = TextEditingController();
@override @override
void initState() { void initState() {
@ -33,11 +31,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
_leetcodeUserSlugController.dispose(); _leetcodeUserSlugController.dispose();
_giteaController.dispose(); _giteaController.dispose();
_giteaUsernameController.dispose(); _giteaUsernameController.dispose();
_kodboxUsernameController.dispose(); _kodboxController.dispose();
_kodboxPasswordController.dispose();
_githubUsernameController.dispose(); _githubUsernameController.dispose();
_githubTokenController.dispose(); _githubTokenController.dispose();
_wakatimeTokenController.dispose();
super.dispose(); super.dispose();
} }
@ -53,16 +49,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
_giteaController.text = authService.credentials['gitea_token'] ?? ''; _giteaController.text = authService.credentials['gitea_token'] ?? '';
_giteaUsernameController.text = _giteaUsernameController.text =
authService.credentials['gitea_username'] ?? ''; authService.credentials['gitea_username'] ?? '';
_kodboxUsernameController.text = _kodboxController.text = authService.credentials['kodbox_token'] ?? '';
authService.credentials['kodbox_username'] ?? '';
_kodboxPasswordController.text =
authService.credentials['kodbox_password'] ?? '';
_githubUsernameController.text = _githubUsernameController.text =
authService.credentials['github_username'] ?? ''; authService.credentials['github_username'] ?? '';
_githubTokenController.text = _githubTokenController.text =
authService.credentials['github_token'] ?? ''; authService.credentials['github_token'] ?? '';
_wakatimeTokenController.text =
authService.credentials['wakatime_token'] ?? '';
}); });
} }
@ -91,17 +82,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_giteaUsernameController.text, _giteaUsernameController.text,
); );
} }
if (_kodboxUsernameController.text.isNotEmpty) { if (_kodboxController.text.isNotEmpty) {
await authService.saveConfigs( await authService.saveConfigs('kodbox_token', _kodboxController.text);
'kodbox_username',
_kodboxUsernameController.text,
);
}
if (_kodboxPasswordController.text.isNotEmpty) {
await authService.saveConfigs(
'kodbox_password',
_kodboxPasswordController.text,
);
} }
if (_githubUsernameController.text.isNotEmpty) { if (_githubUsernameController.text.isNotEmpty) {
await authService.saveConfigs( await authService.saveConfigs(
@ -115,12 +97,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
_githubTokenController.text, _githubTokenController.text,
); );
} }
if (_wakatimeTokenController.text.isNotEmpty) {
await authService.saveConfigs(
'wakatime_token',
_wakatimeTokenController.text,
);
}
if (mounted) { if (mounted) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
@ -195,19 +171,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
icon: Icons.folder, icon: Icons.folder,
children: [ children: [
TextFormField( TextFormField(
controller: _kodboxUsernameController, controller: _kodboxController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: '用户名', labelText: 'API Token',
hintText: '请输入 KodBox 用户名', hintText: '请输入 KodBox API Token',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _kodboxPasswordController,
decoration: const InputDecoration(
labelText: '密码',
hintText: '请输入 KodBox 密码',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
obscureText: true, obscureText: true,
@ -239,22 +206,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
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), const SizedBox(height: 24),
ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')), ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')),
], ],

View File

@ -1,667 +0,0 @@
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>
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<double> _animation;
DateTime _selectedStartDate = DateTime.now();
DateTime _selectedEndDate = DateTime.now();
int? _touchedDayIndex;
//
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();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_animationController.forward();
//
final now = DateTime.now();
_selectedStartDate = now;
_selectedEndDate = now;
_updateDateControllers();
_fetchData();
}
void _updateDateControllers() {
_startDateController.text = _formatDate(_selectedStartDate);
_endDateController.text = _formatDate(_selectedEndDate);
}
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}分钟';
}
void _selectDateRange(String range) {
final now = DateTime.now();
switch (range) {
case 'today':
_selectedStartDate = now;
_selectedEndDate = now;
break;
case '3days':
_selectedStartDate = now.subtract(const Duration(days: 2));
_selectedEndDate = now;
break;
case 'week':
_selectedStartDate = now.subtract(const Duration(days: 6));
_selectedEndDate = now;
break;
}
_updateDateControllers();
_fetchData();
}
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;
});
_animationController.reset();
_animationController.forward();
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Widget _buildLegend(
List<dynamic> 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<dynamic> 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<Offset>(
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),
),
),
],
),
),
),
);
}
Widget _buildDateRangeSelector() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildDateRangeButton('今天', 'today'),
_buildDateRangeButton('近三天', '3days'),
_buildDateRangeButton('近一周', 'week'),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _startDateController,
readOnly: true,
decoration: const InputDecoration(
labelText: '开始日期',
hintText: 'YYYY-MM-DD',
suffixIcon: Icon(Icons.calendar_today),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedStartDate,
firstDate:
DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedStartDate = picked;
if (picked.isAfter(_selectedEndDate)) {
_selectedEndDate = picked;
_endDateController.text = _formatDate(picked);
}
_startDateController.text = _formatDate(picked);
_fetchData();
});
}
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _endDateController,
readOnly: true,
decoration: const InputDecoration(
labelText: '结束日期',
hintText: 'YYYY-MM-DD',
suffixIcon: Icon(Icons.calendar_today),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedEndDate,
firstDate: _selectedStartDate,
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedEndDate = picked;
if (picked.isBefore(_selectedStartDate)) {
_selectedStartDate = picked;
_startDateController.text = _formatDate(picked);
}
_endDateController.text = _formatDate(picked);
_fetchData();
});
}
},
),
),
],
),
],
);
}
Widget _buildDateRangeButton(String text, String range) {
final isSelected =
(range == 'today' && _selectedStartDate == _selectedEndDate) ||
(range == '3days' &&
_selectedEndDate.difference(_selectedStartDate).inDays == 2) ||
(range == 'week' &&
_selectedEndDate.difference(_selectedStartDate).inDays == 6);
return ElevatedButton(
onPressed: () => _selectDateRange(range),
style: ElevatedButton.styleFrom(
backgroundColor: isSelected ? Theme.of(context).primaryColor : null,
foregroundColor: isSelected ? Colors.white : null,
),
child: Text(text),
);
}
Widget _buildDailyChart() {
if (_summary == null || _summary!.data.length <= 1)
return const SizedBox.shrink();
final dailyData = _summary!.data;
final maxSeconds = dailyData
.map((d) => d.grandTotal.totalSeconds)
.reduce((a, b) => a > b ? a : b);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'每日编码时间',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(
height: 40,
child: Center(
child: _touchedDayIndex != null
? Text(
'${_formatDate(DateTime.parse(dailyData[_touchedDayIndex!].range.date))}: ${_formatDuration(dailyData[_touchedDayIndex!].grandTotal.totalSeconds.toInt())}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
)
: null,
),
),
// const SizedBox(height: 16),
SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxSeconds.toDouble(),
barTouchData: BarTouchData(
touchCallback: (FlTouchEvent event, response) {
if (event.isInterestedForInteractions &&
response != null &&
response.spot != null) {
setState(() {
_touchedDayIndex = response.spot!.touchedBarGroupIndex;
});
} else {
setState(() {
_touchedDayIndex = null;
});
}
},
handleBuiltInTouches: false,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.blueGrey,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final date =
DateTime.parse(dailyData[groupIndex].range.date);
final seconds = rod.toY.toInt();
return BarTooltipItem(
'${_formatDate(date)}\n${_formatDuration(seconds)}',
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
},
),
),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
if (value >= 0 && value < dailyData.length) {
final date =
DateTime.parse(dailyData[value.toInt()].range.date);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'${date.month}/${date.day}',
style: const TextStyle(fontSize: 10),
),
);
}
return const Text('');
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Text(
'${(value / 3600).round()}h',
style: const TextStyle(fontSize: 10),
);
},
),
),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(show: false),
barGroups: dailyData.asMap().entries.map((entry) {
final index = entry.key;
final day = entry.value;
final isTouched = index == _touchedDayIndex;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: day.grandTotal.totalSeconds.toDouble(),
width: 20,
color: isTouched
? Theme.of(context).primaryColor
: Theme.of(context).primaryColor.withOpacity(0.5),
),
],
);
}).toList(),
),
),
),
],
);
}
@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: [
_buildDateRangeSelector(),
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),
//
_buildDailyChart(),
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();
}
}

View File

@ -24,9 +24,6 @@ class AuthService extends ChangeNotifier {
final kodboxToken = await _storage.read(key: 'kodbox_token'); final kodboxToken = await _storage.read(key: 'kodbox_token');
final githubUserName = await _storage.read(key: 'github_username'); final githubUserName = await _storage.read(key: 'github_username');
final githubToken = await _storage.read(key: 'github_token'); final githubToken = await _storage.read(key: 'github_token');
final wakatimeToken = await _storage.read(key: 'wakatime_token');
final kodboxUname = await _storage.read(key: 'kodbox_username');
final kodboxPwd = await _storage.read(key: 'kodbox_password');
if (leetcodeCookie != null) { if (leetcodeCookie != null) {
_credentials['leetcode_cookie'] = leetcodeCookie; _credentials['leetcode_cookie'] = leetcodeCookie;
@ -49,15 +46,6 @@ class AuthService extends ChangeNotifier {
if (githubToken != null) { if (githubToken != null) {
_credentials['github_token'] = githubToken; _credentials['github_token'] = githubToken;
} }
if (wakatimeToken != null) {
_credentials['wakatime_token'] = wakatimeToken;
}
if (kodboxUname != null) {
_credentials['kodbox_username'] = kodboxUname;
}
if (kodboxPwd != null) {
_credentials['kodbox_password'] = kodboxPwd;
}
_isAuthenticated = true; _isAuthenticated = true;
notifyListeners(); notifyListeners();
} }

View File

@ -1,65 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'auth_service.dart';
class KodBoxService {
static const String baseUrl = 'https://cloud.jdysya.top';
Future<String> getToken(String username, String password) async {
final response = await http.get(
Uri.parse(
'$baseUrl/?user/index/loginSubmit&name=$username&password=$password'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['code'] == true) {
return data['info'];
} else {
throw Exception('获取token失败: ${data['info'] ?? '未知错误'}');
}
} else {
throw Exception('请求失败: ${response.statusCode}');
}
}
Future<bool> validateToken(String token) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/?admin/log/get&accessToken=$token'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['code'] != '10001';
}
return false;
} catch (e) {
return false;
}
}
Future<String> getValidToken(AuthService authService) async {
final username = authService.credentials['kodbox_username'];
final password = authService.credentials['kodbox_password'];
final currentToken = authService.credentials['kodbox_token'];
if (username == null || password == null) {
throw Exception('请先在设置中配置 KodBox 用户名和密码');
}
// token
if (currentToken != null) {
final isValid = await validateToken(currentToken);
if (isValid) {
return currentToken;
}
}
// token
final newToken = await getToken(username, password);
await authService.saveConfigs('kodbox_token', newToken);
return newToken;
}
}

View File

@ -1,27 +0,0 @@
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<WakatimeSummary> 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}');
}
}
}

View File

@ -125,13 +125,6 @@ class _GiteaCardState extends State<GiteaCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
elevation: 4,
shadowColor: Colors.deepPurple.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)),
),
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(

View File

@ -101,13 +101,6 @@ class _GithubCardState extends State<GithubCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
elevation: 4,
shadowColor: Colors.deepPurple.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)),
),
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(

View File

@ -1,13 +1,9 @@
import 'dart:ffi';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/kodbox_service.dart';
import 'package:timeago/timeago.dart' as timeago; import 'package:timeago/timeago.dart' as timeago;
import 'package:url_launcher/url_launcher.dart';
class KodBoxCard extends StatefulWidget { class KodBoxCard extends StatefulWidget {
const KodBoxCard({super.key}); const KodBoxCard({super.key});
@ -20,7 +16,6 @@ class _KodBoxCardState extends State<KodBoxCard> {
List<dynamic> _logs = []; List<dynamic> _logs = [];
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
final _kodboxService = KodBoxService();
Future<void> _fetchLogs() async { Future<void> _fetchLogs() async {
setState(() { setState(() {
@ -30,7 +25,11 @@ class _KodBoxCardState extends State<KodBoxCard> {
try { try {
final authService = Provider.of<AuthService>(context, listen: false); final authService = Provider.of<AuthService>(context, listen: false);
final token = await _kodboxService.getValidToken(authService); final token = authService.credentials['kodbox_token'];
if (token == null || token.isEmpty) {
throw Exception('未配置 KodBox Token');
}
// 7 // 7
final now = DateTime.now(); final now = DateTime.now();
@ -40,7 +39,7 @@ class _KodBoxCardState extends State<KodBoxCard> {
final request = http.MultipartRequest( final request = http.MultipartRequest(
'POST', 'POST',
Uri.parse('${KodBoxService.baseUrl}/?admin/log/get&accessToken=$token'), Uri.parse('https://cloud.jdysya.top/?admin/log/get&accessToken=$token'),
); );
request.fields.addAll({ request.fields.addAll({
@ -88,120 +87,48 @@ class _KodBoxCardState extends State<KodBoxCard> {
return '未知路径'; return '未知路径';
} }
int? _getSourceID(Map<String, dynamic> desc) {
if (desc['sourceInfo'] == false) {
//
return desc['parentInfo']?['sourceID'];
} else if (desc['sourceInfo'] != null) {
return desc['sourceInfo']?['sourceID'];
}
return null;
}
Future<void> _launchFileUrl(int sourceID) async {
final url = Uri.parse('${KodBoxService.baseUrl}/#explorer&sidf=$sourceID');
if (await canLaunchUrl(url)) {
await launchUrl(url);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('无法打开文件链接')),
);
}
}
}
Widget _buildLogItem(Map<String, dynamic> log) { Widget _buildLogItem(Map<String, dynamic> log) {
final desc = log['desc']; final desc = log['desc'];
final isFileOperation = desc != null && final isFileOperation =
desc != null &&
(desc['sourceInfo'] != null || desc['parentInfo'] != null); (desc['sourceInfo'] != null || desc['parentInfo'] != null);
final sourceID = isFileOperation ? _getSourceID(desc) : null;
return Card( return Card(
elevation: 4, margin: const EdgeInsets.symmetric(vertical: 4.0),
shadowColor: Colors.deepPurple.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)),
),
margin: const EdgeInsets.symmetric(vertical: 5.0),
child: InkWell(
onTap: sourceID != null ? () => _launchFileUrl(sourceID) : null,
splashColor: Colors.deepPurple.withAlpha(30),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: Colors.deepPurple.shade200, radius: 16,
radius: 18, child: Text(log['nickName']?[0].toUpperCase() ?? '?'),
child: Text(
log['nickName']?[0].toUpperCase() ?? '?',
style: const TextStyle(color: Colors.white),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
log['nickName'] ?? log['name'] ?? '未知用户', log['nickName'] ?? log['name'] ?? '未知用户',
style: const TextStyle( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
if (log['title'] != null)
Text(
log['title']!,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
), ),
const SizedBox(width: 8),
Text(log['title'] ?? '未知操作'),
], ],
), ),
if (isFileOperation) ...[ if (isFileOperation) ...[
const SizedBox(height: 12), const SizedBox(height: 8),
Row( Text(
children: [
const Icon(Icons.insert_drive_file,
size: 18, color: Colors.deepPurple),
const SizedBox(width: 8),
Expanded(
child: Text(
'文件路径: ${_getFilePath(desc)}', '文件路径: ${_getFilePath(desc)}',
style: TextStyle( style: const TextStyle(color: Colors.blue),
color: sourceID != null ? Colors.blue : Colors.black,
fontSize: 14,
),
),
),
],
), ),
], ],
const SizedBox(height: 8), const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.access_time, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text( Text(
'时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}', '时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}',
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
], ],
), ),
],
),
),
), ),
); );
} }
@ -215,13 +142,6 @@ class _KodBoxCardState extends State<KodBoxCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.deepPurple.withOpacity(0.15)),
),
elevation: 6,
shadowColor: Colors.deepPurple.withOpacity(0.4),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -232,60 +152,36 @@ class _KodBoxCardState extends State<KodBoxCard> {
children: [ children: [
const Text( const Text(
'KodBox 最近操作', 'KodBox 最近操作',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.deepPurple,
), ),
), IconButton(
Container( icon: const Icon(Icons.refresh),
decoration: BoxDecoration(
color: Colors.deepPurple.shade100,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(8),
child: IconButton(
icon: const Icon(Icons.refresh, color: Colors.deepPurple),
onPressed: _isLoading ? null : _fetchLogs, onPressed: _isLoading ? null : _fetchLogs,
), ),
),
], ],
), ),
if (_isLoading) if (_isLoading)
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(24.0), padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(color: Colors.deepPurple), child: CircularProgressIndicator(),
), ),
) )
else if (_error != null) else if (_error != null)
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 8),
Expanded(
child: Text( child: Text(
_error!, _error!,
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
],
), ),
), ),
) )
else if (_logs.isEmpty) else if (_logs.isEmpty)
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(24.0), padding: EdgeInsets.all(16.0),
child: Text( child: Text('暂无操作记录'),
'暂无操作记录',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
), ),
) )
else else

View File

@ -12,10 +12,7 @@ class LeetCodeCard extends StatefulWidget {
State<LeetCodeCard> createState() => _LeetCodeCardState(); State<LeetCodeCard> createState() => _LeetCodeCardState();
} }
class _LeetCodeCardState extends State<LeetCodeCard> class _LeetCodeCardState extends State<LeetCodeCard> {
with SingleTickerProviderStateMixin {
late AnimationController _refreshController;
List<dynamic> _submissions = []; List<dynamic> _submissions = [];
Map<String, dynamic>? _progress; Map<String, dynamic>? _progress;
bool _isLoading = false; bool _isLoading = false;
@ -121,10 +118,6 @@ class _LeetCodeCardState extends State<LeetCodeCard>
void initState() { void initState() {
super.initState(); super.initState();
_fetchData(); _fetchData();
_refreshController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
} }
Widget _buildProgressCard() { Widget _buildProgressCard() {
@ -135,86 +128,33 @@ class _LeetCodeCardState extends State<LeetCodeCard>
_progress!['totalQuestionBeatsPercentage'] as double; _progress!['totalQuestionBeatsPercentage'] as double;
return Card( return Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Row( const Text(
children: [
Icon(Icons.bar_chart, color: Colors.green),
SizedBox(width: 8),
Text(
'解题进度', '解题进度',
style: TextStyle( style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
fontSize: 18,
fontWeight: FontWeight.bold,
), ),
), const SizedBox(height: 8),
],
),
const SizedBox(height: 12),
...acceptedQuestions.map((q) { ...acceptedQuestions.map((q) {
final difficulty = q['difficulty'] as String; final difficulty = q['difficulty'] as String;
final count = q['count'] as int; final count = q['count'] as int;
Color difficultyColor = Colors.grey;
if (difficulty == 'EASY')
difficultyColor = Colors.green;
else if (difficulty == 'MEDIUM')
difficultyColor = Colors.orange;
else if (difficulty == 'HARD') difficultyColor = Colors.red;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [Text(difficulty), Text('$count')],
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: difficultyColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
difficulty == 'EASY'
? '简单'
: difficulty == 'MEDIUM'
? '中等'
: '困难',
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
Text('$count', style: const TextStyle(fontSize: 14)),
],
), ),
); );
}).toList(), }),
const Divider(), const Divider(),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Row( const Text('击败用户'),
children: [ Text('${totalBeatsPercentage.toStringAsFixed(1)}%'),
Icon(Icons.workspace_premium,
size: 18, color: Colors.green),
SizedBox(width: 6),
Text('击败用户', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
Text('${totalBeatsPercentage.toStringAsFixed(1)}%',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.green)),
], ],
), ),
], ],
@ -226,13 +166,6 @@ class _LeetCodeCardState extends State<LeetCodeCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
elevation: 4,
shadowColor: Colors.deepPurple.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)),
),
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -243,68 +176,28 @@ class _LeetCodeCardState extends State<LeetCodeCard>
children: [ children: [
const Text( const Text(
'LeetCode 最近提交', 'LeetCode 最近提交',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
Container(
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(8),
child: RotationTransition(
turns: _refreshController,
child: IconButton(
icon: const Icon(Icons.refresh,
size: 24, color: Colors.green),
onPressed: _isLoading
? null
: () async {
_refreshController.forward(from: 0.0);
await _fetchData();
},
),
), ),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _fetchData,
), ),
], ],
), ),
if (_isLoading) if (_isLoading)
Center( const Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: EdgeInsets.all(16.0),
child: AnimatedBuilder( child: CircularProgressIndicator(),
animation: _refreshController,
builder: (_, child) => Transform.rotate(
angle: _refreshController.value * 2 * 3.14,
child: child,
),
child: const CircularProgressIndicator(
color: Colors.green,
strokeWidth: 2,
),
),
), ),
) )
else if (_error != null) else if (_error != null)
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 8),
Expanded(
child: Text( child: Text(
_error!, _error!,
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
],
), ),
), ),
) )
@ -319,11 +212,7 @@ class _LeetCodeCardState extends State<LeetCodeCard>
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Text( child: Text('暂无提交记录'),
'暂无提交记录',
style:
TextStyle(fontSize: 16, color: Colors.grey),
),
), ),
) )
else else
@ -335,40 +224,16 @@ class _LeetCodeCardState extends State<LeetCodeCard>
final submission = _submissions[index]; final submission = _submissions[index];
final question = submission['question']; final question = submission['question'];
return Card( return ListTile(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.green,
radius: 16,
child: Text(
question['questionFrontendId'].toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
title: Text( title: Text(
question['translatedTitle'] ?? question['translatedTitle'] ??
question['title'], question['title'],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
), ),
subtitle: Text( subtitle: Text(
'提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}', '提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}',
style: const TextStyle(fontSize: 14),
),
trailing: const Icon(
Icons.check_circle,
color: Colors.green,
), ),
trailing: Text(
'#${question['questionFrontendId']}',
), ),
); );
}, },

View File

@ -7,13 +7,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }

View File

@ -4,7 +4,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux flutter_secure_storage_linux
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -9,12 +9,10 @@ import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -81,14 +81,6 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.0.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -121,14 +113,6 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.1.1" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -146,10 +130,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.0.3" version: "5.0.0"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -268,10 +252,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.1.1" version: "5.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -581,70 +565,6 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.3.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.3.3"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.1.4"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.1.0+2 version: 1.0.0+1
environment: environment:
sdk: '>=3.2.3 <4.0.0' sdk: ^3.7.0
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@ -33,16 +33,14 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.8
http: ^1.1.0 http: ^1.2.0
provider: ^6.1.1 provider: ^6.1.1
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
intl: ^0.19.0 intl: ^0.19.0
flutter_secure_storage: ^9.0.0 flutter_secure_storage: ^9.0.0
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
timeago: ^3.7.1 timeago: ^3.7.1
fl_chart: ^0.65.0
url_launcher: ^6.2.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -53,7 +51,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^2.0.0 flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View File

@ -7,11 +7,8 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -4,7 +4,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows flutter_secure_storage_windows
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST