Compare commits
No commits in common. "main" and "1.0" have entirely different histories.
@ -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}"
|
||||||
|
|||||||
@ -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'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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('未知平台'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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('保存设置')),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,119 +87,47 @@ 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),
|
child: Padding(
|
||||||
shape: RoundedRectangleBorder(
|
padding: const EdgeInsets.all(12.0),
|
||||||
borderRadius: BorderRadius.circular(16),
|
child: Column(
|
||||||
side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
margin: const EdgeInsets.symmetric(vertical: 5.0),
|
Row(
|
||||||
child: InkWell(
|
children: [
|
||||||
onTap: sourceID != null ? () => _launchFileUrl(sourceID) : null,
|
CircleAvatar(
|
||||||
splashColor: Colors.deepPurple.withAlpha(30),
|
radius: 16,
|
||||||
child: Padding(
|
child: Text(log['nickName']?[0].toUpperCase() ?? '?'),
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
backgroundColor: Colors.deepPurple.shade200,
|
|
||||||
radius: 18,
|
|
||||||
child: Text(
|
|
||||||
log['nickName']?[0].toUpperCase() ?? '?',
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
log['nickName'] ?? log['name'] ?? '未知用户',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (log['title'] != null)
|
|
||||||
Text(
|
|
||||||
log['title']!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (isFileOperation) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.insert_drive_file,
|
|
||||||
size: 18, color: Colors.deepPurple),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'文件路径: ${_getFilePath(desc)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: sourceID != null ? Colors.blue : Colors.black,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
log['nickName'] ?? log['name'] ?? '未知用户',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(log['title'] ?? '未知操作'),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
if (isFileOperation) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Text(
|
||||||
children: [
|
'文件路径: ${_getFilePath(desc)}',
|
||||||
const Icon(Icons.access_time, size: 16, color: Colors.grey),
|
style: const TextStyle(color: Colors.blue),
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
'时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}',
|
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}',
|
||||||
|
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Container(
|
IconButton(
|
||||||
decoration: BoxDecoration(
|
icon: const Icon(Icons.refresh),
|
||||||
color: Colors.deepPurple.shade100,
|
onPressed: _isLoading ? null : _fetchLogs,
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.refresh, color: Colors.deepPurple),
|
|
||||||
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(
|
child: Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
_error!,
|
||||||
children: [
|
style: const TextStyle(color: Colors.red),
|
||||||
const Icon(Icons.error_outline, color: Colors.red),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_error!,
|
|
||||||
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
|
||||||
|
|||||||
@ -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),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'解题进度',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8),
|
||||||
...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(
|
IconButton(
|
||||||
decoration: BoxDecoration(
|
icon: const Icon(Icons.refresh),
|
||||||
color: Colors.green.shade100,
|
onPressed: _isLoading ? null : _fetchData,
|
||||||
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();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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(
|
child: Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
_error!,
|
||||||
children: [
|
style: const TextStyle(color: Colors.red),
|
||||||
const Icon(Icons.error_outline, color: Colors.red),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_error!,
|
|
||||||
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,
|
title: Text(
|
||||||
shape: RoundedRectangleBorder(
|
question['translatedTitle'] ??
|
||||||
borderRadius: BorderRadius.circular(16),
|
question['title'],
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
subtitle: Text(
|
||||||
child: ListTile(
|
'提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}',
|
||||||
leading: CircleAvatar(
|
),
|
||||||
backgroundColor: Colors.green,
|
trailing: Text(
|
||||||
radius: 16,
|
'#${question['questionFrontendId']}',
|
||||||
child: Text(
|
|
||||||
question['questionFrontendId'].toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
question['translatedTitle'] ??
|
|
||||||
question['title'],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}',
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
trailing: const Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"))
|
|
||||||
}
|
}
|
||||||
|
|||||||
88
pubspec.lock
88
pubspec.lock
@ -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:
|
||||||
|
|||||||
12
pubspec.yaml
12
pubspec.yaml
@ -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
|
||||||
|
|||||||
@ -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"));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user