- 在首页添加 Wakatime 卡片和相关功能 - 在设置页面添加 Wakatime API Token 配置 - 更新 auth_service 以支持 Wakatime 认证 - 添加 fl_chart 依赖用于 Wakatime 数据图表展示 - 更新 flutter_lints 版本到 2.0.0
542 lines
15 KiB
Dart
542 lines
15 KiB
Dart
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'],
|
|
);
|
|
}
|
|
}
|