From 7472ced80ae90d3570b69f6719b8914c6caac799 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 31 Mar 2024 22:13:44 +0800 Subject: [PATCH 001/458] tiny changes --- lib/life/expense_records/page/statistics.dart | 2 +- lib/school/class2nd/service/points.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 963a0ff7c..8d90f41bc 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -84,7 +84,7 @@ class _ExpenseStatisticsPageState extends State { _buildChartView(), ExpensePieChart(records: type2transactions), // StatisticsSection(mode: selectedMode, all: type2transactions).expanded(), - ].column(), + ].listview(), ); final now = DateTime.now(); final years = _getYear(records); diff --git a/lib/school/class2nd/service/points.dart b/lib/school/class2nd/service/points.dart index dda7b51b7..d33699b36 100644 --- a/lib/school/class2nd/service/points.dart +++ b/lib/school/class2nd/service/points.dart @@ -60,8 +60,8 @@ class Class2ndPointsService { double schoolCulture, }) _parseAllStatus(BeautifulSoup html) { // 学分=1.5(主题报告)+2.0(社会实践)+1.5(创新创业创意)+1.0(校园安全文明)+0.0(公益志愿)+2.0(校园文化) - final found = html.find('#span_score')!; - final String scoreText = found.text; + final found = html.find('#span_score'); + final String scoreText = found!.text; final regExp = RegExp(r'([\d.]+)\(([\u4e00-\u9fa5]+)\)'); late final double lecture, practice, creation, safetyEdu, voluntary, campus; From 90182d852ae5939112197fc2af9e31e84b470e83 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 31 Mar 2024 22:22:08 +0800 Subject: [PATCH 002/458] [workflow] bump flutter to 3.19.5 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 095b5792e..faf8675ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: workflow_dispatch permissions: write-all env: - flutter_version: '3.19.4' + flutter_version: '3.19.5' jobs: build_android: From b1feb6fc0e30e9402716c670e4da25ddde490739 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 31 Mar 2024 23:38:34 +0800 Subject: [PATCH 003/458] [timetable] small fix --- lib/timetable/page/p13n/cell_style.dart | 2 +- lib/timetable/page/timetable.dart | 4 +--- lib/timetable/widgets/timetable/weekly.dart | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/timetable/page/p13n/cell_style.dart b/lib/timetable/page/p13n/cell_style.dart index ad69e5c9e..d70239edf 100644 --- a/lib/timetable/page/p13n/cell_style.dart +++ b/lib/timetable/page/p13n/cell_style.dart @@ -126,7 +126,7 @@ class _TimetableCellStyleEditorState extends State { min: 0.0, max: 1.0, divisions: 255, - label: (value * 255).toInt().toString(), + label: "${(value * 100).toInt()}%", value: value, onChanged: (double value) { setState(() { diff --git a/lib/timetable/page/timetable.dart b/lib/timetable/page/timetable.dart index 43f07866b..80e392799 100644 --- a/lib/timetable/page/timetable.dart +++ b/lib/timetable/page/timetable.dart @@ -113,9 +113,7 @@ class _TimetableBoardPageState extends State { Widget buildMyTimetablesButton() { return IconButton( - icon: isCupertino - ? Icon(Icons.person_outline, color: context.colorScheme.primary) - : const Icon(Icons.person_rounded), + icon: Icon(context.icons.person, color:isCupertino? context.colorScheme.primary: null), onPressed: () async { final focusMode = Settings.focusTimetable; if (focusMode) { diff --git a/lib/timetable/widgets/timetable/weekly.dart b/lib/timetable/widgets/timetable/weekly.dart index 712ebc30f..8be1b0f3a 100644 --- a/lib/timetable/widgets/timetable/weekly.dart +++ b/lib/timetable/widgets/timetable/weekly.dart @@ -365,20 +365,23 @@ class _InteractiveCourseCellState extends State { : () async { $tooltip.currentState?.ensureTooltipVisible(); }, - onLongPress: () async { - await context.show$Sheet$( - (ctx) => TimetableCourseDetailsSheet( - courseCode: widget.lesson.course.courseCode, - timetable: widget.timetable, - ), - ); - }, + // onDoubleTap: showDetailsSheet, + onLongPress: showDetailsSheet, child: child, ), ), ); } + Future showDetailsSheet() async { + await context.show$Sheet$( + (ctx) => TimetableCourseDetailsSheet( + courseCode: widget.lesson.course.courseCode, + timetable: widget.timetable, + ), + ); + } + String buildTooltipMessage() { final lessons = widget.lesson.course.calcBeginEndTimePointForEachLesson(); final lessonTimeTip = lessons.map((time) => "${time.begin.l10n(context)}–${time.end.l10n(context)}").join("\n"); From 0665d48ea9922d37a5e269869333edbb2e251f7c Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 1 Apr 2024 01:16:31 +0800 Subject: [PATCH 004/458] [expense] statistics --- .../expense_records/entity/statistics.dart | 33 ++++++++- lib/life/expense_records/page/statistics.dart | 70 ++++++++++++------- lib/timetable/widgets/timetable/board.dart | 19 ++--- 3 files changed, 80 insertions(+), 42 deletions(-) diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index d4a5b761f..132d87260 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -1,9 +1,36 @@ +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'local.dart'; + +typedef StartTime2Records = List<({DateTime start, List records})>; + enum StatisticsMode { - week, - month, - year; + week(_weekly), + month(_monthly), + year(_yearly); + + final StartTime2Records Function(List records) resort; + + const StatisticsMode(this.resort); String l10nName() => "expenseRecords.statsMode.$name".tr(); } + +StartTime2Records _weekly(List records) { + return []; +} + +StartTime2Records _monthly(List records) { + final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.month)); + final startTime2Records = + ym2records.entries.map((entry) => (start: DateTime(entry.key.$1, entry.key.$2), records: entry.value)).toList(); + return startTime2Records; +} + +StartTime2Records _yearly(List records) { + final ym2records = records.groupListsBy((r) => r.timestamp.year); + final startTime2Records = + ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + return startTime2Records; +} diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 8d90f41bc..a923926c7 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -24,7 +24,7 @@ typedef Type2transactions = Map records, do class _ExpenseStatisticsPageState extends State { late List records; - var selectedMode = StatisticsMode.week; + var selectedMode = StatisticsMode.month; late double total; late Type2transactions type2transactions; late int selectedYear; @@ -79,11 +79,17 @@ class _ExpenseStatisticsPageState extends State { appBar: AppBar( title: i18n.stats.title.text(), ), + // body: [ + // buildModeSelector().padSymmetric(h: 16, v: 4), + // _buildChartView(), + // ExpensePieChart(records: type2transactions), + // ].listview(), body: [ buildModeSelector().padSymmetric(h: 16, v: 4), - _buildChartView(), - ExpensePieChart(records: type2transactions), - // StatisticsSection(mode: selectedMode, all: type2transactions).expanded(), + StatisticsSection( + mode: selectedMode, + all: type2transactions, + ).expanded(), ].listview(), ); final now = DateTime.now(); @@ -207,19 +213,45 @@ class StatisticsSection extends StatefulWidget { } class _StatisticsSectionState extends State { + late Map startTime2Records = resortRecords(); + + Map resortRecords() { + final startTime2Records = widget.all.map((key, value) => MapEntry(key, widget.mode.resort(value.records))); + return startTime2Records; + } + @override - void didChangeDependencies() { - super.didChangeDependencies(); + void didUpdateWidget(covariant StatisticsSection oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.all != widget.all) { + setState(() { + startTime2Records = resortRecords(); + }); + } } @override Widget build(BuildContext context) { - return PageView.builder( - itemCount: 10, - itemBuilder: (ctx, i) { - return const StatisticsPage(); - }, - ); + final list = startTime2Records.values.flattened.toList(); + return const StatisticsPage(); + } +} + +class StatisticsPage extends StatefulWidget { + const StatisticsPage({super.key}); + + @override + State createState() => _StatisticsPageState(); +} + +class _StatisticsPageState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + // return [ + // _buildChartView(), + // ExpensePieChart(records: type2transactions), + // ].column(); } } @@ -324,17 +356,3 @@ class BaseLineChartWidget extends StatelessWidget { ); } } - -class StatisticsPage extends StatefulWidget { - const StatisticsPage({super.key}); - - @override - State createState() => _StatisticsPageState(); -} - -class _StatisticsPageState extends State { - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/timetable/widgets/timetable/board.dart b/lib/timetable/widgets/timetable/board.dart index c5e51e1de..a9d9260cf 100644 --- a/lib/timetable/widgets/timetable/board.dart +++ b/lib/timetable/widgets/timetable/board.dart @@ -86,19 +86,12 @@ class _TimetableBackgroundState extends State with SingleTi @override void didUpdateWidget(covariant TimetableBackground oldWidget) { super.didUpdateWidget(oldWidget); - $opacity.animateTo( - widget.background.opacity, - duration: Durations.medium1, - ); - } - - @override - void didChangeDependencies() { - $opacity.animateTo( - widget.background.opacity, - duration: Durations.medium1, - ); - super.didChangeDependencies(); + if (oldWidget.background.opacity != widget.background.opacity) { + $opacity.animateTo( + widget.background.opacity, + duration: Durations.medium1, + ); + } } @override From aca35a693f62ee055d187c68f05933563a475129 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 1 Apr 2024 12:20:53 +0800 Subject: [PATCH 005/458] [expense] week/month/year statistics --- .../expense_records/entity/statistics.dart | 10 +- lib/life/expense_records/page/statistics.dart | 308 ++++-------------- lib/life/expense_records/utils.dart | 12 + lib/life/expense_records/widget/chart.dart | 160 +++++++++ lib/utils/date.dart | 33 +- 5 files changed, 270 insertions(+), 253 deletions(-) diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index 132d87260..cd380db20 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:sit/utils/date.dart'; import 'local.dart'; @@ -18,7 +19,14 @@ enum StatisticsMode { } StartTime2Records _weekly(List records) { - return []; + final ym2records = records.groupListsBy((r) => getWeek( + year: r.timestamp.year, + month: r.timestamp.month, + day: r.timestamp.day, + )); + final startTime2Records = + ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + return startTime2Records; } StartTime2Records _monthly(List records) { diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index a923926c7..4bc28daa0 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,17 +1,13 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; import 'package:sit/life/expense_records/storage/local.dart'; -import 'package:sit/life/expense_records/utils.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:fl_chart/fl_chart.dart'; +import 'package:sit/life/expense_records/utils.dart'; import '../entity/local.dart'; import '../i18n.dart'; import '../init.dart'; import '../widget/chart.dart'; -import '../widget/selector.dart'; class ExpenseStatisticsPage extends StatefulWidget { const ExpenseStatisticsPage({super.key}); @@ -26,33 +22,16 @@ class _ExpenseStatisticsPageState extends State { late List records; var selectedMode = StatisticsMode.month; late double total; - late Type2transactions type2transactions; - late int selectedYear; - late int selectedMonth; @override void initState() { super.initState(); - final now = DateTime.now(); - selectedYear = now.year; - selectedMonth = now.month; refreshRecords(); } void refreshRecords() { - records = ExpenseRecordsInit.storage.getTransactionsByRange( - start: DateTime(selectedYear, selectedMonth, 1), - end: DateTime(selectedYear, selectedMonth + 1), - ) ?? - const []; + records = ExpenseRecordsInit.storage.getTransactionsByRange() ?? const []; records.retainWhere((record) => record.type.isConsume); - final type2transactions = records.groupListsBy((record) => record.type); - final type2total = type2transactions.map((type, records) => MapEntry(type, accumulateTransactionAmount(records))); - total = type2total.values.sum; - this.type2transactions = type2transactions.map((type, records) { - final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); - return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); - }); } Widget buildModeSelector() { @@ -79,128 +58,49 @@ class _ExpenseStatisticsPageState extends State { appBar: AppBar( title: i18n.stats.title.text(), ), - // body: [ - // buildModeSelector().padSymmetric(h: 16, v: 4), - // _buildChartView(), - // ExpensePieChart(records: type2transactions), - // ].listview(), body: [ buildModeSelector().padSymmetric(h: 16, v: 4), StatisticsSection( mode: selectedMode, - all: type2transactions, - ).expanded(), + all: records, + ), ].listview(), ); - final now = DateTime.now(); - final years = _getYear(records); - final months = _getMonth(records, years, selectedYear); - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - expandedHeight: 200, - flexibleSpace: FlexibleSpaceBar( - title: i18n.stats.title.text(), - centerTitle: true, - background: YearMonthSelector( - years: years, - months: months, - initialYear: now.year, - initialMonth: now.month, - ), - ), - ), - SliverToBoxAdapter( - child: _buildChartView(), - ), - SliverToBoxAdapter( - child: ExpensePieChart(records: type2transactions), - ), - ], - ), - ); - } - - List _getYear(List expenseBillDesc) { - List years = []; - final now = DateTime.now(); - final currentYear = now.year; - final int startYear = expenseBillDesc.isNotEmpty ? expenseBillDesc.last.timestamp.year : currentYear; - for (int year = startYear; year <= currentYear; year++) { - years.add(year); - } - return years; - } - - List _getMonth(List expenseBill, List years, int year) { - List result = []; - final now = DateTime.now(); - if (now.year == year) { - for (int month = 1; month <= now.month; month++) { - result.add(month); - } - } else if (years.first == year) { - for (int month = expenseBill.last.timestamp.month; month <= 12; month++) { - result.add(month); - } - } else { - result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - } - return result; - } - - static int getDayCountOfMonth(int year, int month) { - final int daysFeb = (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) ? 29 : 28; - List days = [31, daysFeb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - return days[month - 1]; - } - - Widget _buildChartView() { - // TODO: take current month into account - // 得到该年该月有多少天, 生成数组记录每一天的消费. - final List daysAmount = List.filled(getDayCountOfMonth(selectedYear, selectedMonth), 0.00); - // 便利该月消费情况, 加到上述统计列表中. - for (final record in records) { - daysAmount[record.timestamp.day - 1] += record.deltaAmount; - } - return OutlinedCard( - child: AspectRatio( - aspectRatio: 1.5, - child: BaseLineChartWidget( - bottomTitles: List.generate(daysAmount.length, (i) => (i + 1).toString()), - values: daysAmount, - ).padSymmetric(v: 12, h: 8), - ), - ); - } - - Widget buildWeekChart() { - return const BaseLineChartWidget( - bottomTitles: [], - values: [], - ); - } - - Widget buildMonthChart() { - return const BaseLineChartWidget( - bottomTitles: [], - values: [], - ); - } - - Widget buildYearChart() { - return const BaseLineChartWidget( - bottomTitles: [], - values: [], - ); + // final now = DateTime.now(); + // final years = _getYear(records); + // final months = _getMonth(records, years, selectedYear); + // return Scaffold( + // body: CustomScrollView( + // slivers: [ + // SliverAppBar( + // pinned: true, + // expandedHeight: 200, + // flexibleSpace: FlexibleSpaceBar( + // title: i18n.stats.title.text(), + // centerTitle: true, + // background: YearMonthSelector( + // years: years, + // months: months, + // initialYear: now.year, + // initialMonth: now.month, + // ), + // ), + // ), + // SliverToBoxAdapter( + // child: _buildChartView(), + // ), + // SliverToBoxAdapter( + // child: ExpensePieChart(records: type2transactions), + // ), + // ], + // ), + // ); } } class StatisticsSection extends StatefulWidget { final StatisticsMode mode; - final Type2transactions all; + final List all; const StatisticsSection({ super.key, @@ -213,17 +113,17 @@ class StatisticsSection extends StatefulWidget { } class _StatisticsSectionState extends State { - late Map startTime2Records = resortRecords(); + late StartTime2Records startTime2Records = resortRecords(); - Map resortRecords() { - final startTime2Records = widget.all.map((key, value) => MapEntry(key, widget.mode.resort(value.records))); + StartTime2Records resortRecords() { + final startTime2Records = widget.mode.resort(widget.all); return startTime2Records; } @override void didUpdateWidget(covariant StatisticsSection oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.all != widget.all) { + if (oldWidget.mode != widget.mode || oldWidget.all != widget.all) { setState(() { startTime2Records = resortRecords(); }); @@ -232,13 +132,25 @@ class _StatisticsSectionState extends State { @override Widget build(BuildContext context) { - final list = startTime2Records.values.flattened.toList(); - return const StatisticsPage(); + return StatisticsPage( + start: startTime2Records.first.start, + mode: widget.mode, + records: startTime2Records.first.records, + ); } } class StatisticsPage extends StatefulWidget { - const StatisticsPage({super.key}); + final DateTime start; + final List records; + final StatisticsMode mode; + + const StatisticsPage({ + super.key, + required this.start, + required this.records, + required this.mode, + }); @override State createState() => _StatisticsPageState(); @@ -247,112 +159,14 @@ class StatisticsPage extends StatefulWidget { class _StatisticsPageState extends State { @override Widget build(BuildContext context) { - return const Placeholder(); - // return [ - // _buildChartView(), - // ExpensePieChart(records: type2transactions), - // ].column(); - } -} - -class BaseLineChartWidget extends StatelessWidget { - final List bottomTitles; - final List values; - - const BaseLineChartWidget({ - super.key, - required this.bottomTitles, - required this.values, - }); - - ///底部标题栏 - Widget bottomTitle(BuildContext ctx, double value, TitleMeta mate) { - if ((value * 10).toInt() % 10 == 5) { - return const SizedBox(); - } - - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text( - bottomTitles[value.toInt()], - style: ctx.textTheme.bodySmall?.copyWith( - color: Colors.blueGrey, - ), + final separated = separateTransactionByType(widget.records); + return [ + ExpenseLineChart( + start: widget.start, + records: widget.records, + mode: widget.mode, ), - ); - } - - ///左边部标题栏 - Widget leftTitle(BuildContext ctx, double value, TitleMeta mate) { - const style = TextStyle( - color: Colors.blueGrey, - fontSize: 11, - ); - String text = '¥${value.toStringAsFixed(2)}'; - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text(text, style: style), - ); - } - - @override - Widget build(BuildContext context) { - return LineChart( - LineChartData( - ///触摸控制 - lineTouchData: LineTouchData( - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (touchedSpot) => Colors.transparent, - ), - touchSpotThreshold: 10, - ), - borderData: FlBorderData( - border: const Border( - bottom: BorderSide.none, - ), - ), - lineBarsData: [ - LineChartBarData( - isStrokeCapRound: true, - belowBarData: BarAreaData( - show: true, - color: context.colorScheme.primary.withOpacity(0.15), - ), - spots: values - .map((e) => (e * 100).toInt() / 100) // 保留两位小数 - .toList() - .asMap() - .entries - .map((e) => FlSpot(e.key.toDouble(), e.value)) - .toList(), - color: context.colorScheme.primary, - isCurved: true, - preventCurveOverShooting: true, - barWidth: 1, - ), - ], - - ///图表线表线框 - titlesData: FlTitlesData( - show: true, - rightTitles: const AxisTitles(), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 50, - getTitlesWidget: (v, meta) => leftTitle(context, v, meta), - ), - ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 55, - getTitlesWidget: (v, meta) => bottomTitle(context, v, meta), - ), - ), - ), - ), - ); + ExpensePieChart(records: separated), + ].column(); } } diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 3c9c5cf82..a807059d4 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -123,3 +123,15 @@ double accumulateTransactionAmount(List transactions) { } return total; } + +Map records, double total})> separateTransactionByType( + List records, +) { + final type2transactions = records.groupListsBy((record) => record.type); + final type2total = type2transactions.map((type, records) => MapEntry(type, accumulateTransactionAmount(records))); + final total = type2total.values.sum; + return type2transactions.map((type, records) { + final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); + return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); + }); +} diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index f7712b362..1485f89e2 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -4,8 +4,10 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/utils/date.dart'; import '../entity/local.dart'; +import '../entity/statistics.dart'; import "../i18n.dart"; class ExpensePieChart extends StatefulWidget { @@ -91,3 +93,161 @@ class _ExpensePieChartState extends State { .wrap(spacing: 4); } } + +class ExpenseLineChart extends StatefulWidget { + final DateTime start; + final StatisticsMode mode; + final List records; + + const ExpenseLineChart({ + super.key, + required this.start, + required this.records, + required this.mode, + }); + + @override + State createState() => _ExpenseLineChartState(); +} + +class _ExpenseLineChartState extends State { + @override + Widget build(BuildContext context) { + final data = buildData(); + return OutlinedCard( + child: AspectRatio( + aspectRatio: 1.5, + child: BaseLineChartWidget( + bottomTitles: List.generate(data.length, (i) => (i + 1).toString()), + values: data, + ).padSymmetric(v: 12, h: 8), + ), + ); + } + + List buildData() { + switch (widget.mode) { + case StatisticsMode.week: + final List daysAmount = List.filled(7, 0.00); + for (final record in widget.records) { + daysAmount[(record.timestamp.day - 1) % 7] += record.deltaAmount; + } + return daysAmount; + case StatisticsMode.month: + final List daysAmount = + List.filled(daysInMonth(year: widget.start.year, month: widget.start.month), 0.00); + for (final record in widget.records) { + daysAmount[record.timestamp.day - 1] += record.deltaAmount; + } + return daysAmount; + case StatisticsMode.year: + final List monthAmounts = List.filled(12, 0.00); + for (final record in widget.records) { + monthAmounts[record.timestamp.month - 1] += record.deltaAmount; + } + return monthAmounts; + } + } +} + +class BaseLineChartWidget extends StatelessWidget { + final List bottomTitles; + final List values; + + const BaseLineChartWidget({ + super.key, + required this.bottomTitles, + required this.values, + }); + + ///底部标题栏 + Widget bottomTitle(BuildContext ctx, double value, TitleMeta mate) { + if ((value * 10).toInt() % 10 == 5) { + return const SizedBox(); + } + + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + bottomTitles[value.toInt()], + style: ctx.textTheme.bodySmall?.copyWith( + color: Colors.blueGrey, + ), + ), + ); + } + + ///左边部标题栏 + Widget leftTitle(BuildContext ctx, double value, TitleMeta mate) { + const style = TextStyle( + color: Colors.blueGrey, + fontSize: 11, + ); + String text = '¥${value.toStringAsFixed(2)}'; + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text(text, style: style), + ); + } + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + ///触摸控制 + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchedSpot) => Colors.transparent, + ), + touchSpotThreshold: 10, + ), + borderData: FlBorderData( + border: const Border( + bottom: BorderSide.none, + ), + ), + lineBarsData: [ + LineChartBarData( + isStrokeCapRound: true, + belowBarData: BarAreaData( + show: true, + color: context.colorScheme.primary.withOpacity(0.15), + ), + spots: values + .map((e) => (e * 100).toInt() / 100) // 保留两位小数 + .toList() + .asMap() + .entries + .map((e) => FlSpot(e.key.toDouble(), e.value)) + .toList(), + color: context.colorScheme.primary, + isCurved: true, + preventCurveOverShooting: true, + barWidth: 1, + ), + ], + + ///图表线表线框 + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles(), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 50, + getTitlesWidget: (v, meta) => leftTitle(context, v, meta), + ), + ), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 55, + getTitlesWidget: (v, meta) => bottomTitle(context, v, meta), + ), + ), + ), + ), + ); + } +} diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 92fc70657..14c59efb7 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -1,6 +1,4 @@ -bool isLeapYear({ - required int year, -}) { +bool isLeapYear(int year) { if (year % 400 == 0) return true; if (year % 4 == 0 && year % 100 != 0) return true; return false; @@ -13,7 +11,7 @@ int daysInMonth({ assert(1 <= month && month <= 12, "month must be in [1,12]"); return switch (month) { 1 => 31, - 2 => isLeapYear(year: year) ? 29 : 28, + 2 => isLeapYear(year) ? 29 : 28, 3 => 31, 4 => 30, 5 => 31, @@ -28,8 +26,33 @@ int daysInMonth({ }; } +int daysInYear(int year) { + return isLeapYear(year) ? 366 : 365; +} + List daysInEachMonth({ required int year, }) { - return [31, isLeapYear(year: year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +} + +int daysPastInYear({ + required int year, + required int month, + required int day, +}) { + var totalMonthLength = 0; + for (var count = 1; count < month; count++) { + totalMonthLength += daysInMonth(month: count, year: year); + } + return totalMonthLength + day; +} + +int getWeek({ + required int year, + required int month, + required int day, +}) { + double a = (daysPastInYear(year: year, month: month, day: day) / 7) + 1; + return a.toInt(); } From e29526e4e0ed8f30658899ff98c8497af1e38832 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 1 Apr 2024 20:11:53 +0800 Subject: [PATCH 006/458] [expense] switch week/month/year in statistics --- lib/life/expense_records/page/statistics.dart | 44 +++++++++++++++++-- lib/life/expense_records/widget/chart.dart | 11 +++-- lib/utils/collection.dart | 16 +++++++ 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 4bc28daa0..b2f4e027a 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:sit/design/adaptive/multiplatform.dart'; +import 'package:sit/design/widgets/card.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; import 'package:sit/life/expense_records/storage/local.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/life/expense_records/utils.dart'; +import 'package:sit/utils/collection.dart'; import '../entity/local.dart'; import '../i18n.dart'; @@ -114,6 +117,7 @@ class StatisticsSection extends StatefulWidget { class _StatisticsSectionState extends State { late StartTime2Records startTime2Records = resortRecords(); + var index = 0; StartTime2Records resortRecords() { final startTime2Records = widget.mode.resort(widget.all); @@ -132,10 +136,42 @@ class _StatisticsSectionState extends State { @override Widget build(BuildContext context) { - return StatisticsPage( - start: startTime2Records.first.start, - mode: widget.mode, - records: startTime2Records.first.records, + final current = startTime2Records.indexAt(index); + return [ + buildHeader(), + StatisticsPage( + start: current.start, + mode: widget.mode, + records: current.records, + ), + ].column(); + } + + Widget buildHeader() { + return FilledCard( + child: [ + IconButton( + onPressed: index > 0 + ? () { + setState(() { + index = index - 1; + }); + } + : null, + icon: Icon(context.icons.leftChevron), + ), + "This year".text(), + IconButton( + onPressed: index < startTime2Records.length + ? () { + setState(() { + index = index + 1; + }); + } + : null, + icon: Icon(context.icons.rightChevron), + ), + ].row(maa: MainAxisAlignment.spaceBetween), ); } } diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index 1485f89e2..98b13115b 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -126,6 +126,8 @@ class _ExpenseLineChartState extends State { } List buildData() { + final now = DateTime.now(); + final start = widget.start; switch (widget.mode) { case StatisticsMode.week: final List daysAmount = List.filled(7, 0.00); @@ -134,14 +136,17 @@ class _ExpenseLineChartState extends State { } return daysAmount; case StatisticsMode.month: - final List daysAmount = - List.filled(daysInMonth(year: widget.start.year, month: widget.start.month), 0.00); + final List daysAmount = List.filled( + start.year == now.year && start.month == now.month + ? now.day + : daysInMonth(year: start.year, month: start.month), + 0.00); for (final record in widget.records) { daysAmount[record.timestamp.day - 1] += record.deltaAmount; } return daysAmount; case StatisticsMode.year: - final List monthAmounts = List.filled(12, 0.00); + final List monthAmounts = List.filled(start.year == now.year ? now.month : 12, 0.00); for (final record in widget.records) { monthAmounts[record.timestamp.month - 1] += record.deltaAmount; } diff --git a/lib/utils/collection.dart b/lib/utils/collection.dart index 7e0f4b8fb..a4450db01 100644 --- a/lib/utils/collection.dart +++ b/lib/utils/collection.dart @@ -12,6 +12,22 @@ extension ListX on List { list.retainWhere((x) => ids.add(id(x))); return list; } + + /// Accesses elements using a negative index similar to Python. + /// A negative index counts from the end of the list. + /// + /// Throws an [ArgumentError] if the index is out of bounds. + E indexAt(int index) { + if (index < 0) { + final absIndex = index.abs(); + if (absIndex > length) { + throw ArgumentError("List index out of range: $index"); + } + return this[length + index]; + } else { + return this[index]; + } + } } extension IterableX on Iterable { From 3dc80f2b0aa069ed6b9cf5eed67bf4836570b377 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 1 Apr 2024 23:37:55 +0800 Subject: [PATCH 007/458] bump flame to 1.17.0, and flame_forge2d to 0.17.1 --- pubspec.lock | 28 ++++++++++++++-------------- pubspec.yaml | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1db7794c7..eaeb233ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -621,18 +621,18 @@ packages: dependency: "direct main" description: name: flame - sha256: bb42a35fa5dabdea535daebe5b020e21a2fce8192b67b7e55cd30fed86d29f69 + sha256: da1812e2f17a8ffd5d43ea6a83137794e7f482bcf50419bc9847b8efdb39f791 url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" flame_forge2d: dependency: "direct main" description: name: flame_forge2d - sha256: "8cdce7f28bd101bd4ca36e0ea5b4f83c56e6d30b795ceac3bcb30fe4e84840bc" + sha256: cca454dbcd175c906958877c08782a8aaa77da40e718539813bfba114c75411d url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.17.1" flex_color_picker: dependency: "direct main" description: @@ -1057,10 +1057,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "42c098e7fb6334746be37cdc30369ade356ed4f14d48b7a0313f95a9159f4321" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.9+5" image_picker_for_web: dependency: transitive description: @@ -1169,10 +1169,10 @@ packages: dependency: transitive description: name: just_audio - sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823 + sha256: b7cb6bbf3750caa924d03f432ba401ec300fd90936b3f73a9b33d58b1e96286b url: "https://pub.dev" source: hosted - version: "0.9.36" + version: "0.9.37" just_audio_platform_interface: dependency: transitive description: @@ -1489,10 +1489,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" permission_handler_windows: dependency: transitive description: @@ -2222,10 +2222,10 @@ packages: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: "582f2f7aecc7376332d961a0dd1efa9378ce117657e0ade55d9ff72699a55e82" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" watcher: dependency: transitive description: @@ -2294,10 +2294,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.4.0" win32_registry: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index bc580b546..715e88a2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -150,8 +150,8 @@ dependencies: screenshot: ^2.1.0 # game - flame: ^1.16.0 - flame_forge2d: ^0.17.0 + flame: ^1.17.0 + flame_forge2d: ^0.17.1 dependency_overrides: intl: ^0.19.0 From f162f3b7c88c0b07bb9562e6bed9af410b415887 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 2 Apr 2024 02:48:08 +0800 Subject: [PATCH 008/458] [update] try to fix update notification on iOS --- lib/entity/version.dart | 1 + lib/update/utils.dart | 80 ++++++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/lib/entity/version.dart b/lib/entity/version.dart index c18e00909..f13a6bcd5 100644 --- a/lib/entity/version.dart +++ b/lib/entity/version.dart @@ -18,6 +18,7 @@ enum AppPlatform { class InstallerStore { static const testFlight = "com.apple.testflight"; + static const appStore = "com.apple"; } class AppMeta { diff --git a/lib/update/utils.dart b/lib/update/utils.dart index be5e1cdd0..3b9e671a0 100644 --- a/lib/update/utils.dart +++ b/lib/update/utils.dart @@ -75,10 +75,7 @@ Future _checkAppUpdateFromApple({ Duration delayAtLeast = Duration.zero, required bool manually, }) async { - final (latest, _) = await ( - UpdateInit.service.getLatestVersionFromAppStore(), - Future.delayed(delayAtLeast), - ).wait; + final latest = await UpdateInit.service.getLatestVersionFromAppStore(); if (latest == null) return; debugPrint(latest.toString()); if (kDebugMode && manually) { @@ -89,43 +86,20 @@ Future _checkAppUpdateFromApple({ final currentVersion = R.currentVersion.version; // if update checking was not manually triggered, skip it. if (!manually && _getSkippedVersion() == latest.version) return; - + if (!manually) { + await Future.delayed(delayAtLeast); + } + if (Dev.on) return; if (!context.mounted) return; - if (!Dev.on && R.currentVersion.installerStore == InstallerStore.testFlight) { + final installerStore = R.currentVersion.installerStore; + if (installerStore == InstallerStore.testFlight) { if (latest.version >= currentVersion) { - final res = await showCupertinoModalPopup( - context: context, - builder: (ctx) => CupertinoActionSheet( - message: i18n.installOnAppStoreInsteadTip.text(), - actions: [ - CupertinoActionSheetAction( - isDefaultAction: true, - onPressed: () { - ctx.pop(true); - }, - child: i18n.openAppStore.text(), - ), - CupertinoActionSheetAction( - onPressed: () { - Settings.skippedVersion = latest.version.toString(); - ctx.pop(false); - }, - child: i18n.skipThisVersion.text(), - ), - ], - cancelButton: CupertinoActionSheetAction( - onPressed: () { - ctx.pop(false); - }, - child: i18n.cancel.text(), - ), - ), - ); - if (res == true) { + final confirm = await _requestInstallOnAppStoreInstead(context: context, latest: latest.version); + if (confirm == true) { await launchUrlString(R.iosAppStoreUrl, mode: LaunchMode.externalApplication); } } - } else { + } else if (installerStore == InstallerStore.appStore) { if (latest.version > currentVersion) { await context.show$Sheet$((ctx) => ArtifactUpdatePage(info: latest)); } else if (manually) { @@ -134,6 +108,40 @@ Future _checkAppUpdateFromApple({ } } +Future _requestInstallOnAppStoreInstead({ + required BuildContext context, + required Version latest, +}) async { + return await showCupertinoModalPopup( + context: context, + builder: (ctx) => CupertinoActionSheet( + message: i18n.installOnAppStoreInsteadTip.text(), + actions: [ + CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () { + ctx.pop(true); + }, + child: i18n.openAppStore.text(), + ), + CupertinoActionSheetAction( + onPressed: () { + Settings.skippedVersion = latest.toString(); + ctx.pop(false); + }, + child: i18n.skipThisVersion.text(), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () { + ctx.pop(false); + }, + child: i18n.cancel.text(), + ), + ), + ); +} + Version? _getSkippedVersion() { final skippedVersionRaw = Settings.skippedVersion; if (skippedVersionRaw != null) { From e6473f29688d90702b63ebc080efc4e370e4756e Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 2 Apr 2024 17:54:43 +0800 Subject: [PATCH 009/458] [expense] this month/last month etc. --- .../expense_records/entity/statistics.dart | 4 ++ lib/life/expense_records/page/statistics.dart | 22 ++++----- lib/life/expense_records/utils.dart | 46 +++++++++++++++++++ lib/life/expense_records/widget/chart.dart | 2 +- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index cd380db20..d863a6312 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -11,6 +11,7 @@ enum StatisticsMode { month(_monthly), year(_yearly); + /// Resort the records, separate them by start time, and sort them in DateTime ascending order. final StartTime2Records Function(List records) resort; const StatisticsMode(this.resort); @@ -26,6 +27,7 @@ StartTime2Records _weekly(List records) { )); final startTime2Records = ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + startTime2Records.sortBy((r) => r.start); return startTime2Records; } @@ -33,6 +35,7 @@ StartTime2Records _monthly(List records) { final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.month)); final startTime2Records = ym2records.entries.map((entry) => (start: DateTime(entry.key.$1, entry.key.$2), records: entry.value)).toList(); + startTime2Records.sortBy((r) => r.start); return startTime2Records; } @@ -40,5 +43,6 @@ StartTime2Records _yearly(List records) { final ym2records = records.groupListsBy((r) => r.timestamp.year); final startTime2Records = ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + startTime2Records.sortBy((r) => r.start); return startTime2Records; } diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index b2f4e027a..c3eb96e2e 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -116,20 +116,16 @@ class StatisticsSection extends StatefulWidget { } class _StatisticsSectionState extends State { - late StartTime2Records startTime2Records = resortRecords(); - var index = 0; - - StartTime2Records resortRecords() { - final startTime2Records = widget.mode.resort(widget.all); - return startTime2Records; - } + late StartTime2Records startTime2Records = widget.mode.resort(widget.all); + late int index = startTime2Records.length - 1; @override void didUpdateWidget(covariant StatisticsSection oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.mode != widget.mode || oldWidget.all != widget.all) { setState(() { - startTime2Records = resortRecords(); + startTime2Records = widget.mode.resort(widget.all); + index = startTime2Records.length - 1; }); } } @@ -138,7 +134,7 @@ class _StatisticsSectionState extends State { Widget build(BuildContext context) { final current = startTime2Records.indexAt(index); return [ - buildHeader(), + buildHeader(current.start), StatisticsPage( start: current.start, mode: widget.mode, @@ -147,8 +143,8 @@ class _StatisticsSectionState extends State { ].column(); } - Widget buildHeader() { - return FilledCard( + Widget buildHeader(DateTime start) { + return OutlinedCard( child: [ IconButton( onPressed: index > 0 @@ -160,9 +156,9 @@ class _StatisticsSectionState extends State { : null, icon: Icon(context.icons.leftChevron), ), - "This year".text(), + resolveTime4Display(context: context, mode: widget.mode, date: start).text(), IconButton( - onPressed: index < startTime2Records.length + onPressed: index < startTime2Records.length - 1 ? () { setState(() { index = index + 1; diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index a807059d4..30b72d0c4 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -1,9 +1,14 @@ import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:sit/life/expense_records/entity/local.dart'; import 'package:sit/school/utils.dart'; +import 'package:sit/utils/date.dart'; import 'entity/remote.dart'; +import 'entity/statistics.dart'; const deviceName2Type = { '开水': TransactionType.water, @@ -135,3 +140,44 @@ Map records, double tota return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); }); } + + +final _monthFormat = DateFormat.MMMM(); +final _yearMonthFormat = DateFormat.yMMMM(); +final _yearFormat = DateFormat.y(); + +String resolveTime4Display({ + required BuildContext context, + required StatisticsMode mode, + required DateTime date, +}) { + final now = DateTime.now(); + switch (mode) { + case StatisticsMode.week: + return now.year == date.year && + getWeek(year: now.year, month: now.month, day: now.day) == + getWeek(year: date.year, month: date.month, day: date.day) + ? "This week" + : "? Week"; + case StatisticsMode.month: + if (date.year == now.year) { + if (date.month == now.month) { + return "This month"; + } else if (date.month == now.month - 1) { + return "Last month"; + } else { + return _monthFormat.format(date); + } + } else { + return _yearMonthFormat.format(date); + } + case StatisticsMode.year: + if (date.year == now.year) { + return "This year"; + } else if (date.year == now.year - 1) { + return "Last year"; + } else { + return _yearFormat.format(date); + } + } +} diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index 98b13115b..cbf131805 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -83,7 +83,7 @@ class _ExpensePieChartState extends State { .map((record) { final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; final color = type.color.harmonizeWith(context.colorScheme.primary); - return RawChip( + return Chip( avatar: Icon(type.icon, color: color), labelStyle: TextStyle(color: color), label: "${type.localized()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), From e3d2d13f8595db900eaa99973927890d4f98d226 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 01:29:40 +0800 Subject: [PATCH 010/458] added wechat official account tile --- lib/me/index.dart | 65 +++++++++++++++++++++++++++-------------------- pubspec.lock | 8 ++++++ pubspec.yaml | 1 + 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/lib/me/index.dart b/lib/me/index.dart index 1d29a1499..83e0926ee 100644 --- a/lib/me/index.dart +++ b/lib/me/index.dart @@ -1,3 +1,4 @@ +import 'package:antdesign_icons/antdesign_icons.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,6 +13,7 @@ import 'package:sit/me/widgets/greeting.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/qrcode/handle.dart'; import 'package:sit/settings/dev.dart'; +import 'package:sit/utils/error.dart'; import 'package:sit/utils/guard_launch.dart'; import 'package:url_launcher/url_launcher_string.dart'; import "i18n.dart"; @@ -19,7 +21,7 @@ import "i18n.dart"; const _qGroupNumber = "917740212"; const _joinQGroupUri = "mqqapi://card/show_pslcard?src_type=internal&version=1&uin=$_qGroupNumber&card_type=group&source=qrcode"; - +const _wechatUri = "weixin://dl/publicaccount?username=gh_61f7fd217d36"; class MePage extends StatefulWidget { const MePage({super.key}); @@ -63,42 +65,51 @@ class _MePageState extends State { ), ], ), - SliverToBoxAdapter( - child: buildGroupInvitationTile(), - ), + SliverList.list(children: [ + buildQQGroupTile(), + buildWechatOfficialAccountTile(), + ]), ], ), ); } - Widget buildGroupInvitationTile() { + Widget buildQQGroupTile() { return ListTile( - title: "预览版 QQ交流群".text(), + leading: const Icon(AntIcons.qqOutlined), + title: "QQ交流群".text(), subtitle: _qGroupNumber.text(), - trailing: [ - IconButton( - onPressed: () async { - try { - await launchUrlString(_joinQGroupUri); - } catch (_) {} - }, - icon: const Icon(Icons.group), - ), - IconButton( - tooltip: i18n.copy, - onPressed: () async { + trailing: IconButton( + onPressed: () async { + try { + await launchUrlString(_joinQGroupUri); + } catch (error,stackTrace) { + debugPrintError(error,stackTrace); await Clipboard.setData(const ClipboardData(text: _qGroupNumber)); if (!mounted) return; context.showSnackBar(content: "已复制到剪贴板".text()); - }, - icon: Icon(context.icons.copy), - ), - ].row(mas: MainAxisSize.min), - onTap: () async { - try { - await launchUrlString(_joinQGroupUri); - } catch (_) {} - }, + } + }, + icon: const Icon(Icons.group), + ), + ); + } + + Widget buildWechatOfficialAccountTile() { + return ListTile( + leading: const Icon(AntIcons.wechatOutlined), + title: "微信公众号".text(), + subtitle: "小应生活".text(), + trailing: IconButton( + onPressed: () async { + try { + await launchUrlString(_wechatUri); + } catch (error,stackTrace) { + debugPrintError(error,stackTrace); + } + }, + icon: Icon(context.icons.rightChevron), + ), ); } diff --git a/pubspec.lock b/pubspec.lock index eaeb233ee..0a1ea12d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.11" + antdesign_icons: + dependency: "direct main" + description: + name: antdesign_icons + sha256: fc956ce592a48fd999ec623d0580652a5075525fa262878765eef6144891b41b + url: "https://pub.dev" + source: hosted + version: "0.0.3" app_links: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 715e88a2d..2c5eea0b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -121,6 +121,7 @@ dependencies: animations: ^2.0.11 dynamic_color: ^1.7.0 unicons: ^2.1.1 + antdesign_icons: ^0.0.3 sliver_tools: ^0.2.12 flutter_staggered_grid_view: ^0.7.0 flex_color_picker: ^3.4.1 From efb9e2b27d3cc3241731aa2d1b304cb11fb35216 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 01:33:35 +0800 Subject: [PATCH 011/458] bump chewie to 1.8.0. bump package_info_plus to 6.0.0. --- pubspec.lock | 36 ++++++++++++++++++------------------ pubspec.yaml | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 0a1ea12d4..903ec122a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -261,10 +261,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" + sha256: "1fc84d88d3b1dc26b1fe799500e2ebcc8916af30ce62595ad802cfd965b60bc3" url: "https://pub.dev" source: hosted - version: "1.7.5" + version: "1.8.0" clock: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -1025,10 +1025,10 @@ packages: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -1385,10 +1385,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: cb44f49b6e690fa766f023d5b22cac6b9affe741dd792b6ac7ad4fabe0d7b097 url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "6.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1761,10 +1761,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -2094,10 +2094,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -2222,10 +2222,10 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" wakelock_plus_platform_interface: dependency: transitive description: @@ -2246,18 +2246,18 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c5eea0b4..b85530632 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: webview_flutter: ^4.7.0 fk_user_agent: ^2.1.0 flutter_widget_from_html: ^0.14.11 - chewie: ^1.7.5 + chewie: ^1.8.0 cookie_jar: ^4.0.8 flutter_html: ^3.0.0-beta.2 @@ -92,7 +92,7 @@ dependencies: window_manager: ^0.3.8 win32_registry: ^1.1.2 # Get package info (version) - package_info_plus: ^5.0.1 + package_info_plus: ^6.0.0 # Check VPN connection status check_vpn_connection: ^0.0.2 connectivity_plus: ^5.0.2 From 45c39c50aa463576ed5657057067ee4e30fed064 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 02:26:25 +0800 Subject: [PATCH 012/458] Revert "bump chewie to 1.8.0. bump package_info_plus to 6.0.0." This reverts commit efb9e2b27d3cc3241731aa2d1b304cb11fb35216. --- pubspec.lock | 36 ++++++++++++++++++------------------ pubspec.yaml | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 903ec122a..0a1ea12d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -261,10 +261,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "1fc84d88d3b1dc26b1fe799500e2ebcc8916af30ce62595ad802cfd965b60bc3" + sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.7.5" clock: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.3+8" crypto: dependency: "direct main" description: @@ -1025,10 +1025,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -1385,10 +1385,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: cb44f49b6e690fa766f023d5b22cac6b9affe741dd792b6ac7ad4fabe0d7b097 + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "5.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1761,10 +1761,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: @@ -2094,10 +2094,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -2222,10 +2222,10 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" + sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.4" wakelock_plus_platform_interface: dependency: transitive description: @@ -2246,18 +2246,18 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.3" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b85530632..2c5eea0b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: webview_flutter: ^4.7.0 fk_user_agent: ^2.1.0 flutter_widget_from_html: ^0.14.11 - chewie: ^1.8.0 + chewie: ^1.7.5 cookie_jar: ^4.0.8 flutter_html: ^3.0.0-beta.2 @@ -92,7 +92,7 @@ dependencies: window_manager: ^0.3.8 win32_registry: ^1.1.2 # Get package info (version) - package_info_plus: ^6.0.0 + package_info_plus: ^5.0.1 # Check VPN connection status check_vpn_connection: ^0.0.2 connectivity_plus: ^5.0.2 From 257c75848a9e0dc412a7d3e988b8b088e5f65bf5 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 03:59:57 +0800 Subject: [PATCH 013/458] [expense] this week/last week --- lib/life/expense_records/entity/local.dart | 2 +- .../expense_records/entity/statistics.dart | 11 ++++------ lib/life/expense_records/utils.dart | 21 +++++++++++++----- lib/life/expense_records/widget/chart.dart | 22 +++++++++++-------- lib/utils/date.dart | 19 ++++++++++++++++ 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/lib/life/expense_records/entity/local.dart b/lib/life/expense_records/entity/local.dart index a7dc3a416..45a450b39 100644 --- a/lib/life/expense_records/entity/local.dart +++ b/lib/life/expense_records/entity/local.dart @@ -50,7 +50,7 @@ class Transaction { @override String toString() { return jsonEncode({ - "datetime": timestamp.toString(), + "timestamp": timestamp.toString(), "consumerId": consumerId, "type": type.toString(), "balanceBefore": balanceBefore, diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index d863a6312..cce98f622 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -20,13 +20,10 @@ enum StatisticsMode { } StartTime2Records _weekly(List records) { - final ym2records = records.groupListsBy((r) => getWeek( - year: r.timestamp.year, - month: r.timestamp.month, - day: r.timestamp.day, - )); - final startTime2Records = - ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); + final startTime2Records = ym2records.entries + .map((entry) => (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) + .toList(); startTime2Records.sortBy((r) => r.start); return startTime2Records; } diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 30b72d0c4..9759c4831 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -141,8 +141,8 @@ Map records, double tota }); } - final _monthFormat = DateFormat.MMMM(); +final _monthDayFormat = DateFormat.Md(); final _yearMonthFormat = DateFormat.yMMMM(); final _yearFormat = DateFormat.y(); @@ -154,11 +154,20 @@ String resolveTime4Display({ final now = DateTime.now(); switch (mode) { case StatisticsMode.week: - return now.year == date.year && - getWeek(year: now.year, month: now.month, day: now.day) == - getWeek(year: date.year, month: date.month, day: date.day) - ? "This week" - : "? Week"; + if (date.year == now.year) { + final nowWeek = now.week; + final dateWeek = date.week; + if (dateWeek == nowWeek) { + return "This week"; + } else if (dateWeek == nowWeek - 1) { + return "Last week"; + } else { + return "? week ${_yearFormat.format(date)}"; + // return "${_monthDayFormat.format(date)}"; + } + } else { + return "? week ${_yearFormat.format(date)}"; + } case StatisticsMode.month: if (date.year == now.year) { if (date.month == now.month) { diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index cbf131805..ae6942db0 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -130,27 +130,31 @@ class _ExpenseLineChartState extends State { final start = widget.start; switch (widget.mode) { case StatisticsMode.week: - final List daysAmount = List.filled(7, 0.00); + final List weekAmount = List.filled(7, 0.00); for (final record in widget.records) { - daysAmount[(record.timestamp.day - 1) % 7] += record.deltaAmount; + // add data at the same weekday. + // sunday goes first + weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; } - return daysAmount; + return weekAmount; case StatisticsMode.month: - final List daysAmount = List.filled( + final List dayAmount = List.filled( start.year == now.year && start.month == now.month ? now.day : daysInMonth(year: start.year, month: start.month), 0.00); for (final record in widget.records) { - daysAmount[record.timestamp.day - 1] += record.deltaAmount; + // add data on the same day. + dayAmount[record.timestamp.day - 1] += record.deltaAmount; } - return daysAmount; + return dayAmount; case StatisticsMode.year: - final List monthAmounts = List.filled(start.year == now.year ? now.month : 12, 0.00); + final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); for (final record in widget.records) { - monthAmounts[record.timestamp.month - 1] += record.deltaAmount; + // add data in the same month. + monthAmount[record.timestamp.month - 1] += record.deltaAmount; } - return monthAmounts; + return monthAmount; } } } diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 14c59efb7..5f0ec35c4 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -56,3 +56,22 @@ int getWeek({ double a = (daysPastInYear(year: year, month: month, day: day) / 7) + 1; return a.toInt(); } + +extension DateTimeX on DateTime { + int get week => getWeek(year: year, month: month, day: day); +} + +DateTime getDateOfFirstDayInWeek({ + required int year, + required int week, +}) { + final day = (week - 1) * 7; + return DateTime(year, 1, day); +} + +String formatTimeSpan({ + required DateTime from, + required DateTime to, +}) { + return ""; +} From 56389708b502dc5a85f1a215d0e10753ca3b02c8 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 04:15:57 +0800 Subject: [PATCH 014/458] [expense] listing in statistics --- lib/life/expense_records/entity/local.dart | 8 +-- lib/life/expense_records/page/statistics.dart | 54 ++++++------------- .../expense_records/widget/transaction.dart | 8 ++- 3 files changed, 23 insertions(+), 47 deletions(-) diff --git a/lib/life/expense_records/entity/local.dart b/lib/life/expense_records/entity/local.dart index 45a450b39..52abd8145 100644 --- a/lib/life/expense_records/entity/local.dart +++ b/lib/life/expense_records/entity/local.dart @@ -69,13 +69,7 @@ extension TransactionX on Transaction { (balanceAfter - balanceBefore) < 0 && type != TransactionType.topUp && type != TransactionType.subsidy; String? get bestTitle { - if (deviceName.isNotEmpty) { - return deviceName; - } else if (note.isNotEmpty) { - return note; - } else { - return null; - } + return deviceName.isNotEmpty? deviceName : note.isNotEmpty ? note : null; } String shortDeviceName() { diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index c3eb96e2e..4202d5b62 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -11,6 +11,7 @@ import '../entity/local.dart'; import '../i18n.dart'; import '../init.dart'; import '../widget/chart.dart'; +import '../widget/transaction.dart'; class ExpenseStatisticsPage extends StatefulWidget { const ExpenseStatisticsPage({super.key}); @@ -66,8 +67,8 @@ class _ExpenseStatisticsPageState extends State { StatisticsSection( mode: selectedMode, all: records, - ), - ].listview(), + ).expanded(), + ].column(caa: CrossAxisAlignment.stretch), ); // final now = DateTime.now(); // final years = _getYear(records); @@ -133,13 +134,21 @@ class _StatisticsSectionState extends State { @override Widget build(BuildContext context) { final current = startTime2Records.indexAt(index); + final separated = separateTransactionByType(current.records); return [ buildHeader(current.start), - StatisticsPage( - start: current.start, - mode: widget.mode, - records: current.records, - ), + [ + ExpenseLineChart( + start: current.start, + records: current.records, + mode: widget.mode, + ), + ExpensePieChart(records: separated), + ...current.records.map((record) { + return TransactionTile(record); + }), + ].listview().expanded(), + // ListView() ].column(); } @@ -171,34 +180,3 @@ class _StatisticsSectionState extends State { ); } } - -class StatisticsPage extends StatefulWidget { - final DateTime start; - final List records; - final StatisticsMode mode; - - const StatisticsPage({ - super.key, - required this.start, - required this.records, - required this.mode, - }); - - @override - State createState() => _StatisticsPageState(); -} - -class _StatisticsPageState extends State { - @override - Widget build(BuildContext context) { - final separated = separateTransactionByType(widget.records); - return [ - ExpenseLineChart( - start: widget.start, - records: widget.records, - mode: widget.mode, - ), - ExpensePieChart(records: separated), - ].column(); - } -} diff --git a/lib/life/expense_records/widget/transaction.dart b/lib/life/expense_records/widget/transaction.dart index 6d3ab964a..16597fddc 100644 --- a/lib/life/expense_records/widget/transaction.dart +++ b/lib/life/expense_records/widget/transaction.dart @@ -12,9 +12,13 @@ class TransactionTile extends StatelessWidget { @override Widget build(BuildContext context) { + final title = transaction.bestTitle; return ListTile( - title: Text(transaction.bestTitle ?? i18n.unknown, style: context.textTheme.titleSmall), - subtitle: context.formatYmdhmsNum(transaction.timestamp).text(), + title: Text(title ?? i18n.unknown, style: context.textTheme.titleSmall), + subtitle: [ + context.formatYmdhmsNum(transaction.timestamp).text(), + if (title != transaction.note) transaction.note.text(), + ].column(caa: CrossAxisAlignment.start), leading: transaction.type.icon.make(color: transaction.type.color, size: 32), trailing: transaction.toReadableString().text( style: context.textTheme.titleLarge?.copyWith(color: transaction.billColor), From efc948a61081a290e5291960671962da481919cf Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 04:29:25 +0800 Subject: [PATCH 015/458] PlatformIconButton --- lib/design/adaptive/multiplatform.dart | 2 ++ lib/game/2048/game.dart | 3 ++- lib/game/2048/widget/button.dart | 3 ++- lib/game/minesweeper/game.dart | 3 ++- lib/game/widget/card.dart | 5 +++-- lib/life/electricity/index.dart | 9 +++++++-- lib/life/expense_records/page/records.dart | 9 +++++++-- lib/life/expense_records/page/statistics.dart | 5 +++-- lib/login/page/index.dart | 5 +++-- lib/me/edu_email/page/login.dart | 3 ++- lib/me/index.dart | 9 +++++---- lib/qrcode/page/scanner.dart | 12 ++++-------- lib/r.dart | 2 +- lib/school/class2nd/index.dart | 9 +++++++-- lib/school/class2nd/page/activity.dart | 3 ++- lib/school/class2nd/page/attended.dart | 4 ++-- lib/school/class2nd/widgets/search.dart | 6 +++++- lib/school/exam_result/page/gpa.dart | 2 +- lib/school/library/page/details.dart | 7 ++++--- lib/school/library/page/login.dart | 3 ++- lib/school/library/page/search.dart | 9 +++++---- lib/school/oa_announce/page/details.dart | 3 ++- lib/school/yellow_pages/page/index.dart | 3 ++- lib/school/yellow_pages/widgets/contact.dart | 5 +++-- lib/school/yellow_pages/widgets/search.dart | 3 ++- lib/settings/page/about.dart | 7 ++++--- lib/settings/page/credentials.dart | 5 +++-- lib/settings/page/developer.dart | 16 +++++++++------- lib/settings/page/proxy.dart | 12 ++++++------ lib/timetable/page/editor.dart | 10 +++++----- lib/timetable/page/ical.dart | 4 ++-- lib/timetable/page/mine.dart | 5 +++-- lib/timetable/page/preview.dart | 3 ++- lib/timetable/page/timetable.dart | 2 +- lib/widgets/modal_image_view.dart | 3 ++- lib/widgets/search.dart | 5 +++-- lib/widgets/webview/page.dart | 7 ++++--- 37 files changed, 124 insertions(+), 82 deletions(-) diff --git a/lib/design/adaptive/multiplatform.dart b/lib/design/adaptive/multiplatform.dart index 24b2c6080..0507b29e3 100644 --- a/lib/design/adaptive/multiplatform.dart +++ b/lib/design/adaptive/multiplatform.dart @@ -32,4 +32,6 @@ extension PlatformIconsX on PlatformIcons { IconData get calendar => isMaterial(context) ? Icons.calendar_month : CupertinoIcons.calendar; IconData get qrcode => isMaterial(context) ? Icons.qr_code : CupertinoIcons.qrcode; + + IconData get preview => isMaterial(context) ? Icons.preview : CupertinoIcons.eye; } diff --git a/lib/game/2048/game.dart b/lib/game/2048/game.dart index b8d2e186c..204dd0df9 100644 --- a/lib/game/2048/game.dart +++ b/lib/game/2048/game.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; import 'package:rettulf/rettulf.dart'; @@ -119,7 +120,7 @@ class _GameState extends ConsumerState with TickerProviderStateMixin, appBar: AppBar( title: i18n.title.text(), actions: [ - IconButton( + PlatformIconButton( onPressed: () { ref.read(boardManager.notifier).newGame(); }, diff --git a/lib/game/2048/widget/button.dart b/lib/game/2048/widget/button.dart index bf6cd7215..5e4a5126d 100644 --- a/lib/game/2048/widget/button.dart +++ b/lib/game/2048/widget/button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import '../theme.dart'; @@ -19,7 +20,7 @@ class ButtonWidget extends ConsumerWidget { color: scoreColor, borderRadius: BorderRadius.circular(12.0), ), - child: IconButton( + child: PlatformIconButton( color: textColorWhite, onPressed: onPressed, icon: Icon( diff --git a/lib/game/minesweeper/game.dart b/lib/game/minesweeper/game.dart index a2251a5fe..9bc727874 100644 --- a/lib/game/minesweeper/game.dart +++ b/lib/game/minesweeper/game.dart @@ -1,3 +1,4 @@ +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; @@ -118,7 +119,7 @@ class _MinesweeperState extends ConsumerState with WidgetsBindi centerTitle: true, title: i18n.title.text(), actions: [ - IconButton( + PlatformIconButton( onPressed: () { resetGame(); }, diff --git a/lib/game/widget/card.dart b/lib/game/widget/card.dart index 100299ae2..d0d95a070 100644 --- a/lib/game/widget/card.dart +++ b/lib/game/widget/card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/widgets/app.dart'; @@ -77,14 +78,14 @@ class _OfflineGameAppCardState extends State { ], rightActions: [ if (widget.supportHistory) - IconButton( + PlatformIconButton( onPressed: () { context.push("/game${widget.baseRoute}/history"); }, icon: const Icon(Icons.history), ), if (widget.supportLeaderboard) - IconButton( + PlatformIconButton( onPressed: () { context.push("/game${widget.baseRoute}/leaderboard"); }, diff --git a/lib/life/electricity/index.dart b/lib/life/electricity/index.dart index f7a346f2d..5bddea366 100644 --- a/lib/life/electricity/index.dart +++ b/lib/life/electricity/index.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' hide isCupertino; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/app.dart'; @@ -115,8 +116,12 @@ class _ElectricityBalanceAppCardState extends ConsumerState { ? i18n.title.text() : i18n.balanceInCard(lastTransaction.balanceAfter.toStringAsFixed(2)).text(), actions: [ - IconButton( - tooltip: i18n.delete, + PlatformIconButton( + material: (ctx, p) { + return MaterialIconButtonData( + tooltip: i18n.delete, + ); + }, icon: Icon(context.icons.delete), onPressed: () async { ExpenseRecordsInit.storage.clearIndex(); diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 4202d5b62..af29447bb 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; @@ -155,7 +156,7 @@ class _StatisticsSectionState extends State { Widget buildHeader(DateTime start) { return OutlinedCard( child: [ - IconButton( + PlatformIconButton( onPressed: index > 0 ? () { setState(() { @@ -166,7 +167,7 @@ class _StatisticsSectionState extends State { icon: Icon(context.icons.leftChevron), ), resolveTime4Display(context: context, mode: widget.mode, date: start).text(), - IconButton( + PlatformIconButton( onPressed: index < startTime2Records.length - 1 ? () { setState(() { diff --git a/lib/login/page/index.dart b/lib/login/page/index.dart index 7830dd57d..8cc7f79b3 100644 --- a/lib/login/page/index.dart +++ b/lib/login/page/index.dart @@ -20,6 +20,7 @@ import 'package:sit/school/widgets/campus.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/settings/dev.dart'; import 'package:sit/settings/settings.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' hide isCupertino; import '../aggregated.dart'; import '../i18n.dart'; @@ -163,7 +164,7 @@ class _LoginPageState extends State { appBar: AppBar( title: widget.isGuarded ? i18n.loginRequired.text() : const CampusSelector(), actions: [ - IconButton( + PlatformIconButton( icon: isCupertino ? const Icon(CupertinoIcons.settings) : const Icon(Icons.settings), onPressed: () { context.push("/settings"); @@ -247,7 +248,7 @@ class _LoginPageState extends State { labelText: i18n.credentials.oaPwd, hintText: i18n.oaPwdHint, icon: Icon(context.icons.lock), - suffixIcon: IconButton( + suffixIcon: PlatformIconButton( icon: Icon(isPasswordClear ? context.icons.eyeSolid : context.icons.eyeSlashSolid), onPressed: () { setState(() { diff --git a/lib/me/edu_email/page/login.dart b/lib/me/edu_email/page/login.dart index 9e79ad9fd..7dfa70103 100644 --- a/lib/me/edu_email/page/login.dart +++ b/lib/me/edu_email/page/login.dart @@ -1,6 +1,7 @@ import 'package:email_validator/email_validator.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; @@ -117,7 +118,7 @@ class _EduEmailLoginPageState extends State { labelText: i18n.login.credentials.pwd, icon: Icon(context.icons.lock), hintText: i18n.login.passwordHint, - suffixIcon: IconButton( + suffixIcon: PlatformIconButton( icon: Icon(isPasswordClear ? context.icons.eyeSolid : context.icons.eyeSlashSolid), onPressed: () { setState(() { diff --git a/lib/me/index.dart b/lib/me/index.dart index 83e0926ee..22668f032 100644 --- a/lib/me/index.dart +++ b/lib/me/index.dart @@ -2,6 +2,7 @@ import 'package:antdesign_icons/antdesign_icons.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; @@ -40,7 +41,7 @@ class _MePageState extends State { titleTextStyle: context.textTheme.headlineSmall, actions: [ buildScannerAction(), - IconButton( + PlatformIconButton( icon: Icon(context.icons.settings), onPressed: () { context.push("/settings"); @@ -79,7 +80,7 @@ class _MePageState extends State { leading: const Icon(AntIcons.qqOutlined), title: "QQ交流群".text(), subtitle: _qGroupNumber.text(), - trailing: IconButton( + trailing: PlatformIconButton( onPressed: () async { try { await launchUrlString(_joinQGroupUri); @@ -100,7 +101,7 @@ class _MePageState extends State { leading: const Icon(AntIcons.wechatOutlined), title: "微信公众号".text(), subtitle: "小应生活".text(), - trailing: IconButton( + trailing: PlatformIconButton( onPressed: () async { try { await launchUrlString(_wechatUri); @@ -114,7 +115,7 @@ class _MePageState extends State { } Widget buildScannerAction() { - return IconButton( + return PlatformIconButton( onPressed: () async { final res = await context.push("/tools/scanner"); if (!mounted) return; diff --git a/lib/qrcode/page/scanner.dart b/lib/qrcode/page/scanner.dart index e63885a08..587e14684 100644 --- a/lib/qrcode/page/scanner.dart +++ b/lib/qrcode/page/scanner.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -20,8 +21,6 @@ class ScannerPage extends StatefulWidget { State createState() => _ScannerPageState(); } -const _iconSize = 32.0; - class _ScannerPageState extends State with SingleTickerProviderStateMixin { final controller = MobileScannerController( torchEnabled: false, @@ -105,9 +104,8 @@ class _ScannerPageState extends State with SingleTickerProviderStat } Widget buildImagePicker() { - return IconButton( + return PlatformIconButton( icon: const Icon(Icons.image), - iconSize: _iconSize, onPressed: () async { final ImagePicker picker = ImagePicker(); // Pick an image @@ -123,16 +121,14 @@ class _ScannerPageState extends State with SingleTickerProviderStat } Widget buildSwitchButton() { - return IconButton( - iconSize: _iconSize, + return PlatformIconButton( icon: Icon(context.icons.switchCamera), onPressed: () => controller.switchCamera(), ); } Widget buildTorchButton() { - return IconButton( - iconSize: _iconSize, + return PlatformIconButton( icon: controller.torchState >> (context, state) { switch (state) { diff --git a/lib/r.dart b/lib/r.dart index 3c33ac10b..dea530930 100644 --- a/lib/r.dart +++ b/lib/r.dart @@ -20,7 +20,7 @@ class R { static late AppMeta currentVersion; /// For debugging iOS on other platforms. - static const debugCupertino = kDebugMode ? false : false; + static const debugCupertino = kDebugMode ? true : false; /// The default window size is small enough for any modern desktop device. static const Size defaultWindowSize = Size(500, 800); diff --git a/lib/school/class2nd/index.dart b/lib/school/class2nd/index.dart index fdc1de5d8..d7e09b35d 100644 --- a/lib/school/class2nd/index.dart +++ b/lib/school/class2nd/index.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' hide isCupertino; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/widgets/oa_scope.dart'; @@ -111,8 +112,12 @@ class _Class2ndAppCardState extends ConsumerState { ], rightActions: [ if (!isCupertino) - IconButton( - tooltip: i18n.share, + PlatformIconButton( + material: (ctx, p) { + return MaterialIconButtonData( + tooltip: i18n.share, + ); + }, onPressed: summary != null ? () async { await shareSummery(summary: summary, target: getTargetScore(), context: context); diff --git a/lib/school/class2nd/page/activity.dart b/lib/school/class2nd/page/activity.dart index ef4b16e4e..e93bb9dfb 100644 --- a/lib/school/class2nd/page/activity.dart +++ b/lib/school/class2nd/page/activity.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; @@ -54,7 +55,7 @@ class _ActivityListPageState extends State { title: i18n.title.text(), forceElevated: innerBoxIsScrolled, actions: [ - IconButton( + PlatformIconButton( icon: Icon(context.icons.search), onPressed: () => showSearch(context: context, delegate: ActivitySearchDelegate()), ), diff --git a/lib/school/class2nd/page/attended.dart b/lib/school/class2nd/page/attended.dart index 127db08b9..b1d5a0133 100644 --- a/lib/school/class2nd/page/attended.dart +++ b/lib/school/class2nd/page/attended.dart @@ -106,7 +106,7 @@ class _AttendedActivityPageState extends State { floating: true, title: i18n.attended.title.text(), actions: [ - IconButton( + PlatformIconButton( onPressed: () async { final result = await showSearch( context: context, @@ -463,7 +463,7 @@ class AttendedActivitySearchDelegate extends SearchDelegate { @override List? buildActions(BuildContext context) { return [ - IconButton(onPressed: () => query = '', icon: Icon(context.icons.clear)), + PlatformIconButton(onPressed: () => query = '', icon: Icon(context.icons.clear)), ]; } diff --git a/lib/school/class2nd/widgets/search.dart b/lib/school/class2nd/widgets/search.dart index 9cce0c796..d48dbe790 100644 --- a/lib/school/class2nd/widgets/search.dart +++ b/lib/school/class2nd/widgets/search.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/common.dart'; @@ -13,7 +14,10 @@ class ActivitySearchDelegate extends SearchDelegate { @override List? buildActions(BuildContext context) { return [ - IconButton(onPressed: () => query = '', icon: Icon(context.icons.clear)), + PlatformIconButton( + onPressed: () => query = '', + icon: Icon(context.icons.clear), + ), ]; } diff --git a/lib/school/exam_result/page/gpa.dart b/lib/school/exam_result/page/gpa.dart index b5c8bde8d..05bc1cfaf 100644 --- a/lib/school/exam_result/page/gpa.dart +++ b/lib/school/exam_result/page/gpa.dart @@ -229,7 +229,7 @@ class _ExamResultGroupBySemesterState extends State { subtitle: _buildGpaText(items: selectedItems).text(), titleTextStyle: context.textTheme.titleMedium, onTap: toggleExpand, - trailing: IconButton( + trailing: PlatformIconButton( icon: Icon( isGroupNoneSelected ? context.icons.checkBoxBlankOutlineRounded diff --git a/lib/school/library/page/details.dart b/lib/school/library/page/details.dart index b5396b56e..baea21c3a 100644 --- a/lib/school/library/page/details.dart +++ b/lib/school/library/page/details.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/list_tile.dart'; @@ -115,7 +116,7 @@ class _BookDetailsPageState extends State { subtitle: book.title, trailing: onSearchTap == null ? null - : IconButton( + : PlatformIconButton( icon: Icon(context.icons.search), onPressed: () { onSearchTap.call(SearchMethod.title, book.title); @@ -127,7 +128,7 @@ class _BookDetailsPageState extends State { subtitle: book.author, trailing: onSearchTap == null ? null - : IconButton( + : PlatformIconButton( icon: Icon(context.icons.search), onPressed: () { onSearchTap.call(SearchMethod.author, book.author); @@ -148,7 +149,7 @@ class _BookDetailsPageState extends State { subtitle: publisher, trailing: onSearchTap == null ? null - : IconButton( + : PlatformIconButton( icon: const Icon(Icons.youtube_searched_for), onPressed: () { onSearchTap.call(SearchMethod.publisher, publisher); diff --git a/lib/school/library/page/login.dart b/lib/school/library/page/login.dart index 1e3e57b75..ddd7d8283 100644 --- a/lib/school/library/page/login.dart +++ b/lib/school/library/page/login.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; @@ -108,7 +109,7 @@ class _LibraryLoginPageState extends State { labelText: i18n.login.credentials.pwd, hintText: i18n.login.passwordHint, icon: Icon(context.icons.lock), - suffixIcon: IconButton( + suffixIcon: PlatformIconButton( icon: Icon(isPasswordClear ? context.icons.eyeSolid : context.icons.eyeSlashSolid), onPressed: () { setState(() { diff --git a/lib/school/library/page/search.dart b/lib/school/library/page/search.dart index c791f29f8..246cf8280 100644 --- a/lib/school/library/page/search.dart +++ b/lib/school/library/page/search.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/school/library/storage/search.dart'; @@ -39,7 +40,7 @@ class LibrarySearchDelegate extends SearchDelegate { @override List? buildActions(BuildContext context) { return [ - IconButton( + PlatformIconButton( icon: Icon(context.icons.clear), onPressed: () { query = ''; @@ -51,7 +52,7 @@ class LibrarySearchDelegate extends SearchDelegate { @override Widget? buildLeading(BuildContext context) { - return IconButton( + return PlatformIconButton( icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, progress: transitionAnimation, @@ -163,7 +164,7 @@ class _LibraryTrendsGroupState extends State { return SuggestionItemView( tileLeading: Icon(recentOrTotal ? Icons.local_fire_department : Icons.people), title: recentOrTotal ? i18n.searching.trending.text() : i18n.searching.mostPopular.text(), - tileTrailing: IconButton( + tileTrailing: PlatformIconButton( icon: const Icon(Icons.swap_horiz), onPressed: () { setState(() { @@ -226,7 +227,7 @@ class _LibrarySearchHistoryGroupState extends State { tileLeading: const Icon(Icons.history), title: i18n.searching.searchHistory.text(), items: history, - tileTrailing: IconButton( + tileTrailing: PlatformIconButton( icon: Icon(context.icons.delete), onPressed: history?.isNotEmpty == true ? () { diff --git a/lib/school/oa_announce/page/details.dart b/lib/school/oa_announce/page/details.dart index 03b73b6a0..502acb1d7 100644 --- a/lib/school/oa_announce/page/details.dart +++ b/lib/school/oa_announce/page/details.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/widgets/list_tile.dart'; import 'package:sit/design/widgets/tags.dart'; import 'package:sit/l10n/extension.dart'; @@ -84,7 +85,7 @@ class _AnnounceDetailsPageState extends State { floating: true, title: i18n.title.text(), actions: [ - IconButton( + PlatformIconButton( onPressed: () { launchUrlString( OaAnnounceService.getAnnounceUrl(widget.record.catalogId, widget.record.uuid), diff --git a/lib/school/yellow_pages/page/index.dart b/lib/school/yellow_pages/page/index.dart index 171fd1c1d..d0b75678e 100644 --- a/lib/school/yellow_pages/page/index.dart +++ b/lib/school/yellow_pages/page/index.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/r.dart'; import 'package:sit/school/yellow_pages/init.dart'; @@ -23,7 +24,7 @@ class _YellowPagesListPageState extends State { appBar: AppBar( title: i18n.title.text(), actions: [ - IconButton( + PlatformIconButton( onPressed: () async { final result = await showSearch(context: context, delegate: YellowPageSearchDelegate(R.yellowPages)); if (result == null) return; diff --git a/lib/school/yellow_pages/widgets/contact.dart b/lib/school/yellow_pages/widgets/contact.dart index ca7ab7fb3..c17732bb8 100644 --- a/lib/school/yellow_pages/widgets/contact.dart +++ b/lib/school/yellow_pages/widgets/contact.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/school/yellow_pages/init.dart'; @@ -46,14 +47,14 @@ class ContactTile extends StatelessWidget { trailing: phoneNumber.isEmpty ? null : [ - IconButton( + PlatformIconButton( icon: const Icon(Icons.phone), onPressed: () async { YellowPagesInit.storage.addInteractHistory(contact); await guardLaunchUrlString(context, "tel:$phoneNumber"); }, ), - IconButton( + PlatformIconButton( icon: const Icon(Icons.content_copy), onPressed: () async { YellowPagesInit.storage.addInteractHistory(contact); diff --git a/lib/school/yellow_pages/widgets/search.dart b/lib/school/yellow_pages/widgets/search.dart index cdf4e9941..567c63ce3 100644 --- a/lib/school/yellow_pages/widgets/search.dart +++ b/lib/school/yellow_pages/widgets/search.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import '../entity/contact.dart'; @@ -12,7 +13,7 @@ class YellowPageSearchDelegate extends SearchDelegate { @override List? buildActions(BuildContext context) { return [ - IconButton( + PlatformIconButton( icon: Icon(context.icons.clear), onPressed: () => query = "", ), diff --git a/lib/settings/page/about.dart b/lib/settings/page/about.dart index cfa2908c3..753a6e529 100644 --- a/lib/settings/page/about.dart +++ b/lib/settings/page/about.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/widgets/list_tile.dart'; @@ -47,7 +48,7 @@ class _AboutSettingsPageState extends State { DetailListTile( title: i18n.about.icpLicense, subtitle: R.icpLicense, - trailing: IconButton( + trailing: PlatformIconButton( icon: const Icon(Icons.open_in_browser), onPressed: () async { await guardLaunchUrlString(context, "https://beian.miit.gov.cn/"); @@ -56,7 +57,7 @@ class _AboutSettingsPageState extends State { ), ListTile( title: i18n.about.termsOfUse.text(), - trailing: IconButton( + trailing: PlatformIconButton( icon: const Icon(Icons.open_in_browser), onPressed: () async { await guardLaunchUrlString( @@ -66,7 +67,7 @@ class _AboutSettingsPageState extends State { ), ListTile( title: i18n.about.privacyPolicy.text(), - trailing: IconButton( + trailing: PlatformIconButton( icon: const Icon(Icons.open_in_browser), onPressed: () async { await guardLaunchUrlString( diff --git a/lib/settings/page/credentials.dart b/lib/settings/page/credentials.dart index 093cf5a7d..4d0c91cdf 100644 --- a/lib/settings/page/credentials.dart +++ b/lib/settings/page/credentials.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/credentials/widgets/oa_scope.dart'; @@ -82,7 +83,7 @@ class _CredentialsPageState extends State { subtitle: Text(!showPassword ? i18n.oaCredentials.savedOaPwdDesc : credential.password), leading: const Icon(Icons.password_rounded), trailing: [ - IconButton( + PlatformIconButton( icon: Icon(context.icons.edit), onPressed: () async { final newPwd = await Editor.showStringEditor( @@ -97,7 +98,7 @@ class _CredentialsPageState extends State { } }, ), - IconButton( + PlatformIconButton( onPressed: () { setState(() { showPassword = !showPassword; diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index 1ffce0e92..a6a999f4b 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/init.dart'; @@ -142,13 +143,14 @@ class _DebugGoRouteTileState extends State { ), trailing: [ $route >> - (ctx, route) => IconButton( - onPressed: route.text.isEmpty - ? null - : () { - context.push(route.text); - }, - icon: const Icon(Icons.arrow_forward)) + (ctx, route) => PlatformIconButton( + onPressed: route.text.isEmpty + ? null + : () { + context.push(route.text); + }, + icon: const Icon(Icons.arrow_forward), + ) ].row(mas: MainAxisSize.min), ); } diff --git a/lib/settings/page/proxy.dart b/lib/settings/page/proxy.dart index 6dbaff9da..f3ea31fd0 100644 --- a/lib/settings/page/proxy.dart +++ b/lib/settings/page/proxy.dart @@ -261,7 +261,7 @@ class _ProxyProfileEditorPageState extends State { subtitle: uri.toString(), trailing: [ if (!type.isDefaultUri(uri)) - IconButton( + PlatformIconButton( onPressed: () { setState(() { this.uri = type.buildDefaultUri(); @@ -269,7 +269,7 @@ class _ProxyProfileEditorPageState extends State { }, icon: Icon(context.icons.delete), ), - IconButton( + PlatformIconButton( icon: Icon(context.icons.edit), onPressed: () async { var newFullProxy = await Editor.showStringEditor( @@ -329,7 +329,7 @@ class _ProxyProfileEditorPageState extends State { leading: const Icon(Icons.link), title: i18n.proxy.hostname, subtitle: host, - trailing: IconButton( + trailing: PlatformIconButton( icon: Icon(context.icons.edit), onPressed: () async { final newHostRaw = await Editor.showStringEditor( @@ -357,7 +357,7 @@ class _ProxyProfileEditorPageState extends State { leading: const Icon(Icons.settings_input_component_outlined), title: i18n.proxy.port, subtitle: port.toString(), - trailing: IconButton( + trailing: PlatformIconButton( icon: Icon(context.icons.edit), onPressed: () async { final newPort = await Editor.showIntEditor( @@ -388,7 +388,7 @@ class _ProxyProfileEditorPageState extends State { subtitle: text?.text(), trailing: [ if (auth != null) - IconButton( + PlatformIconButton( onPressed: () { setState(() { uri = uri.replace(userInfo: ""); @@ -396,7 +396,7 @@ class _ProxyProfileEditorPageState extends State { }, icon: Icon(context.icons.delete), ), - IconButton( + PlatformIconButton( icon: Icon(context.icons.edit), onPressed: () async { final newAuth = await showAdaptiveDialog<({String username, String password})>( diff --git a/lib/timetable/page/editor.dart b/lib/timetable/page/editor.dart index 23c6a2f45..286aa0573 100644 --- a/lib/timetable/page/editor.dart +++ b/lib/timetable/page/editor.dart @@ -310,7 +310,7 @@ class TimetableEditableCourseCard extends StatelessWidget { leading: CourseIcon(courseName: template.courseName), title: template.courseName.text(), trailing: [ - IconButton.filledTonal( + PlatformIconButton( icon: Icon(context.icons.add), padding: EdgeInsets.zero, onPressed: () async { @@ -325,7 +325,7 @@ class TimetableEditableCourseCard extends StatelessWidget { onCourseAdded?.call(newItem); }, ), - IconButton.filledTonal( + PlatformIconButton( icon: Icon(context.icons.edit), padding: EdgeInsets.zero, onPressed: () async { @@ -365,7 +365,7 @@ class TimetableEditableCourseCard extends StatelessWidget { "${Weekday.fromIndex(course.dayIndex).l10n()} ${begin.l10n(context)}–${end.l10n(context)}".text(), ...weekNumbers.map((n) => n.text()), ].column(mas: MainAxisSize.min, caa: CrossAxisAlignment.start), - trailing: IconButton.filledTonal( + trailing: PlatformIconButton( icon: Icon(context.icons.edit), padding: EdgeInsets.zero, onPressed: () async { @@ -604,7 +604,7 @@ class _SitCourseEditorPageState extends State { return [ ListTile( title: i18n.editor.repeating.text(), - trailing: IconButton.filledTonal( + trailing: PlatformIconButton( icon: Icon(context.icons.add), onPressed: () { final newIndices = List.of(weekIndices.indices); @@ -642,7 +642,7 @@ class _SitCourseEditorPageState extends State { return ListTile( title: i18n.course.teacher(2).text(), isThreeLine: true, - trailing: IconButton( + trailing: PlatformIconButton( icon: Icon(context.icons.add), onPressed: () async { final newTeacher = await Editor.showStringEditor( diff --git a/lib/timetable/page/ical.dart b/lib/timetable/page/ical.dart index 465f2e1c0..54b4a3daa 100644 --- a/lib/timetable/page/ical.dart +++ b/lib/timetable/page/ical.dart @@ -169,7 +169,7 @@ class _TimetableICalConfigEditorState extends State { enabled: enableAlarm, title: i18n.export.alarmDuration.text(), subtitle: i18n.time.minuteFormat(alarmDuration.inMinutes.toString()).text(), - trailing: IconButton( + trailing: PlatformIconButton( icon: Icon(context.icons.edit), onPressed: !enableAlarm ? null @@ -193,7 +193,7 @@ class _TimetableICalConfigEditorState extends State { enabled: enableAlarm, title: i18n.export.alarmBeforeClassBegins.text(), subtitle: i18n.export.alarmBeforeClassBeginsDesc(alarmBeforeClass).text(), - trailing: IconButton( + trailing: PlatformIconButton( icon: Icon(context.icons.edit), onPressed: !enableAlarm ? null diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart index 33574c136..1c88eacb1 100644 --- a/lib/timetable/page/mine.dart +++ b/lib/timetable/page/mine.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/menu.dart'; @@ -97,7 +98,7 @@ class _MyTimetableListPageState extends State { if (Settings.focusTimetable) buildMoreActionsButton() else - IconButton( + PlatformIconButton( icon: const Icon(Icons.color_lens_outlined), onPressed: () { context.push("/timetable/p13n"); @@ -243,7 +244,7 @@ class TimetableCard extends StatelessWidget { EntryAction( main: true, label: i18n.preview, - icon: isCupertino ? CupertinoIcons.eye : Icons.preview, + icon: ctx.icons.preview, activator: const SingleActivator(LogicalKeyboardKey.keyP), action: () async { if (!ctx.mounted) return; diff --git a/lib/timetable/page/preview.dart b/lib/timetable/page/preview.dart index 1ffe6fb14..cb5680583 100644 --- a/lib/timetable/page/preview.dart +++ b/lib/timetable/page/preview.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:text_scroll/text_scroll.dart'; import '../entity/display.dart'; @@ -43,7 +44,7 @@ class _TimetablePreviewPageState extends State { appBar: AppBar( title: TextScroll(widget.timetable.name), actions: [ - IconButton( + PlatformIconButton( icon: const Icon(Icons.swap_horiz), onPressed: () { $displayMode.value = $displayMode.value.toggle(); diff --git a/lib/timetable/page/timetable.dart b/lib/timetable/page/timetable.dart index 80e392799..f5bff87b2 100644 --- a/lib/timetable/page/timetable.dart +++ b/lib/timetable/page/timetable.dart @@ -112,7 +112,7 @@ class _TimetableBoardPageState extends State { } Widget buildMyTimetablesButton() { - return IconButton( + return PlatformIconButton( icon: Icon(context.icons.person, color:isCupertino? context.colorScheme.primary: null), onPressed: () async { final focusMode = Settings.focusTimetable; diff --git a/lib/widgets/modal_image_view.dart b/lib/widgets/modal_image_view.dart index b2d1144cb..b633b90b9 100644 --- a/lib/widgets/modal_image_view.dart +++ b/lib/widgets/modal_image_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; @@ -74,7 +75,7 @@ class FullScreenViewer extends StatelessWidget { ), Align( alignment: Alignment.topRight, - child: IconButton( + child: PlatformIconButton( icon: Icon( context.icons.clear, ), diff --git a/lib/widgets/search.dart b/lib/widgets/search.dart index 7d78686e9..94a4054bc 100644 --- a/lib/widgets/search.dart +++ b/lib/widgets/search.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:rettulf/rettulf.dart'; @@ -133,7 +134,7 @@ class ItemSearchDelegate extends SearchDelegate { @override List? buildActions(BuildContext context) { return [ - IconButton( + PlatformIconButton( icon: Icon(context.icons.clear), onPressed: () => query = "", ) @@ -142,7 +143,7 @@ class ItemSearchDelegate extends SearchDelegate { @override Widget? buildLeading(BuildContext context) { - return IconButton( + return PlatformIconButton( icon: AnimatedIcon(icon: AnimatedIcons.menu_arrow, progress: transitionAnimation), onPressed: () => close(context, null), ); diff --git a/lib/widgets/webview/page.dart b/lib/widgets/webview/page.dart index 14295b83c..1019e98b1 100644 --- a/lib/widgets/webview/page.dart +++ b/lib/widgets/webview/page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/l10n/common.dart'; import 'package:sit/utils/error.dart'; @@ -124,17 +125,17 @@ class _WebViewPageState extends State { Widget build(BuildContext context) { final actions = [ if (widget.showRefreshButton) - IconButton( + PlatformIconButton( onPressed: _onRefresh, icon: Icon(context.icons.refresh), ), if (widget.showSharedButton) - IconButton( + PlatformIconButton( onPressed: _onShared, icon: Icon(context.icons.share), ), if (widget.showOpenInBrowser) - IconButton( + PlatformIconButton( onPressed: () => launchUrlString( widget.initialUrl, mode: LaunchMode.externalApplication, From ab373b847b32861c8bc6f6ba8590d0f30c9427f5 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 04:34:17 +0800 Subject: [PATCH 016/458] bump version to 2.4.0 --- lib/r.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/r.dart b/lib/r.dart index dea530930..3c33ac10b 100644 --- a/lib/r.dart +++ b/lib/r.dart @@ -20,7 +20,7 @@ class R { static late AppMeta currentVersion; /// For debugging iOS on other platforms. - static const debugCupertino = kDebugMode ? true : false; + static const debugCupertino = kDebugMode ? false : false; /// The default window size is small enough for any modern desktop device. static const Size defaultWindowSize = Size(500, 800); diff --git a/pubspec.yaml b/pubspec.yaml index 2c5eea0b4..7e561b5c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A multiplatform app for SIT students." # The build version numbers is incremented automatically. # DO NOT DIRECTLY CHANGE IT -version: 2.3.2+27 +version: 2.4.0+27 homepage: https://github.com/liplum/mimir repository: https://github.com/liplum/mimir From bf0061dc4b2b8ac8051e7892a247d85a749416c5 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 04:41:27 +0800 Subject: [PATCH 017/458] [timetable] Timetable patch --- lib/timetable/entity/patch.dart | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/timetable/entity/patch.dart diff --git a/lib/timetable/entity/patch.dart b/lib/timetable/entity/patch.dart new file mode 100644 index 000000000..e5a300277 --- /dev/null +++ b/lib/timetable/entity/patch.dart @@ -0,0 +1,46 @@ +enum TimetablePatchType { + removeLesson, + removeDay, + moveLesson, + moveDay, + addLesson, +} + +abstract class TimetablePatch { + TimetablePatchType get type; +} + +class RemoveLessonTimetablePatch implements TimetablePatch { + @override + final type = TimetablePatchType.removeLesson; + + const RemoveLessonTimetablePatch(); +} + +class RemoveDayTimetablePatch implements TimetablePatch { + @override + final type = TimetablePatchType.removeDay; + + const RemoveDayTimetablePatch(); +} + +class MoveLessonTimetablePatch implements TimetablePatch { + @override + final type = TimetablePatchType.moveLesson; + + const MoveLessonTimetablePatch(); +} + +class MoveDayTimetablePatch implements TimetablePatch { + @override + final type = TimetablePatchType.moveDay; + + const MoveDayTimetablePatch(); +} + +class AddLessonTimetablePatch implements TimetablePatch { + @override + final type = TimetablePatchType.addLesson; + + const AddLessonTimetablePatch(); +} From 967b3193d9900eb9dcd36799e1f103906b8dd34d Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 04:43:15 +0800 Subject: [PATCH 018/458] [game] resort --- lib/game/2048/entity/record.dart | 2 ++ lib/game/minesweeper/{model => entity}/board.dart | 0 lib/game/minesweeper/{model => entity}/cell.dart | 0 lib/game/minesweeper/{model => entity}/mode.dart | 0 lib/game/minesweeper/entity/record.dart | 3 +++ lib/game/minesweeper/{model => entity}/screen.dart | 0 lib/game/minesweeper/game.dart | 6 +++--- lib/game/minesweeper/i18n.dart | 2 +- lib/game/minesweeper/manager/logic.dart | 8 ++++---- lib/game/minesweeper/save.dart | 4 ++-- lib/game/minesweeper/widget/cell.dart | 2 +- lib/game/minesweeper/widget/cell/button.dart | 2 +- lib/game/minesweeper/widget/cell/number.dart | 2 +- 13 files changed, 18 insertions(+), 13 deletions(-) rename lib/game/minesweeper/{model => entity}/board.dart (100%) rename lib/game/minesweeper/{model => entity}/cell.dart (100%) rename lib/game/minesweeper/{model => entity}/mode.dart (100%) create mode 100644 lib/game/minesweeper/entity/record.dart rename lib/game/minesweeper/{model => entity}/screen.dart (100%) diff --git a/lib/game/2048/entity/record.dart b/lib/game/2048/entity/record.dart index 8b1378917..e3df98937 100644 --- a/lib/game/2048/entity/record.dart +++ b/lib/game/2048/entity/record.dart @@ -1 +1,3 @@ +class Record2048 { +} diff --git a/lib/game/minesweeper/model/board.dart b/lib/game/minesweeper/entity/board.dart similarity index 100% rename from lib/game/minesweeper/model/board.dart rename to lib/game/minesweeper/entity/board.dart diff --git a/lib/game/minesweeper/model/cell.dart b/lib/game/minesweeper/entity/cell.dart similarity index 100% rename from lib/game/minesweeper/model/cell.dart rename to lib/game/minesweeper/entity/cell.dart diff --git a/lib/game/minesweeper/model/mode.dart b/lib/game/minesweeper/entity/mode.dart similarity index 100% rename from lib/game/minesweeper/model/mode.dart rename to lib/game/minesweeper/entity/mode.dart diff --git a/lib/game/minesweeper/entity/record.dart b/lib/game/minesweeper/entity/record.dart new file mode 100644 index 000000000..3bb8c6280 --- /dev/null +++ b/lib/game/minesweeper/entity/record.dart @@ -0,0 +1,3 @@ +class RecordMinesweeper { + +} diff --git a/lib/game/minesweeper/model/screen.dart b/lib/game/minesweeper/entity/screen.dart similarity index 100% rename from lib/game/minesweeper/model/screen.dart rename to lib/game/minesweeper/entity/screen.dart diff --git a/lib/game/minesweeper/game.dart b/lib/game/minesweeper/game.dart index 9bc727874..f40dca389 100644 --- a/lib/game/minesweeper/game.dart +++ b/lib/game/minesweeper/game.dart @@ -5,9 +5,9 @@ import 'package:logger/logger.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/game/minesweeper/save.dart'; -import 'model/board.dart'; -import 'model/cell.dart'; -import 'model/mode.dart'; +import 'entity/board.dart'; +import 'entity/cell.dart'; +import 'entity/mode.dart'; import 'widget/info.dart'; import 'manager/logic.dart'; import 'widget/board.dart'; diff --git a/lib/game/minesweeper/i18n.dart b/lib/game/minesweeper/i18n.dart index 41e9fbde3..2c433b8a0 100644 --- a/lib/game/minesweeper/i18n.dart +++ b/lib/game/minesweeper/i18n.dart @@ -1,6 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:sit/game/i18n.dart'; -import 'package:sit/game/minesweeper/model/mode.dart'; +import 'entity/mode.dart'; import 'package:sit/l10n/common.dart'; const i18n = _I18n(); diff --git a/lib/game/minesweeper/manager/logic.dart b/lib/game/minesweeper/manager/logic.dart index dccc69490..57365d4c6 100644 --- a/lib/game/minesweeper/manager/logic.dart +++ b/lib/game/minesweeper/manager/logic.dart @@ -1,12 +1,12 @@ import 'package:sit/game/minesweeper/save.dart'; -import '../model/mode.dart'; -import '../model/screen.dart'; +import '../entity/mode.dart'; +import '../entity/screen.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import "package:flutter/foundation.dart"; import 'package:logger/logger.dart'; -import '../model/board.dart'; -import '../model/cell.dart'; +import '../entity/board.dart'; +import '../entity/cell.dart'; // Debug Tool final logger = Logger(); diff --git a/lib/game/minesweeper/save.dart b/lib/game/minesweeper/save.dart index 73a2db8eb..13f0ef24e 100644 --- a/lib/game/minesweeper/save.dart +++ b/lib/game/minesweeper/save.dart @@ -1,9 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/game/minesweeper/model/cell.dart'; import 'package:sit/game/storage/storage.dart'; import 'package:version/version.dart'; -import 'model/mode.dart'; +import 'entity/mode.dart'; +import 'entity/cell.dart'; part "save.g.dart"; diff --git a/lib/game/minesweeper/widget/cell.dart b/lib/game/minesweeper/widget/cell.dart index 2f7eb832c..d2b46c6d3 100644 --- a/lib/game/minesweeper/widget/cell.dart +++ b/lib/game/minesweeper/widget/cell.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/material.dart'; import "package:flutter/foundation.dart"; import 'package:logger/logger.dart'; -import '../model/cell.dart'; +import '../entity/cell.dart'; import '../manager/logic.dart'; import 'cell/button.dart'; import 'cell/cover.dart'; diff --git a/lib/game/minesweeper/widget/cell/button.dart b/lib/game/minesweeper/widget/cell/button.dart index d95b0ec90..abf704d96 100644 --- a/lib/game/minesweeper/widget/cell/button.dart +++ b/lib/game/minesweeper/widget/cell/button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rettulf/build_context.dart'; -import '../../model/cell.dart'; +import '../../entity/cell.dart'; import '../../manager/logic.dart'; class CellButton extends ConsumerWidget { diff --git a/lib/game/minesweeper/widget/cell/number.dart b/lib/game/minesweeper/widget/cell/number.dart index e3d07fcd8..224151e7e 100644 --- a/lib/game/minesweeper/widget/cell/number.dart +++ b/lib/game/minesweeper/widget/cell/number.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../manager/logic.dart'; -import '../../model/cell.dart'; +import '../../entity/cell.dart'; import '../../theme.dart'; class MinesAroundNumber extends ConsumerWidget { From ca6f32822a49670b22bf0dcb4a13ae1450a0ea7c Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 04:50:17 +0800 Subject: [PATCH 019/458] [game] 2048 records --- lib/game/2048/entity/record.dart | 11 ++++++++++- lib/game/minesweeper/entity/record.dart | 5 ++++- lib/game/record/record.dart | 5 +++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 lib/game/record/record.dart diff --git a/lib/game/2048/entity/record.dart b/lib/game/2048/entity/record.dart index e3df98937..9065b704f 100644 --- a/lib/game/2048/entity/record.dart +++ b/lib/game/2048/entity/record.dart @@ -1,3 +1,12 @@ -class Record2048 { +import 'package:sit/game/record/record.dart'; +class Record2048 extends GameRecord { + final int score; + final int maxNumber; + + const Record2048({ + required super.ts, + required this.score, + required this.maxNumber, + }); } diff --git a/lib/game/minesweeper/entity/record.dart b/lib/game/minesweeper/entity/record.dart index 3bb8c6280..d7796f504 100644 --- a/lib/game/minesweeper/entity/record.dart +++ b/lib/game/minesweeper/entity/record.dart @@ -1,3 +1,6 @@ -class RecordMinesweeper { +import 'package:sit/game/record/record.dart'; +class RecordMinesweeper extends GameRecord { + + const RecordMinesweeper({required super.ts}); } diff --git a/lib/game/record/record.dart b/lib/game/record/record.dart new file mode 100644 index 000000000..625f53865 --- /dev/null +++ b/lib/game/record/record.dart @@ -0,0 +1,5 @@ +class GameRecord { + final DateTime ts; + + const GameRecord({required this.ts}); +} From 101def42894d648ec87d9cbb21efe4792d62ae0a Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 12:17:15 +0800 Subject: [PATCH 020/458] [game] [2048] JsonSerializable records --- lib/game/2048/entity/record.dart | 6 ++++++ lib/game/2048/entity/record.g.dart | 19 +++++++++++++++++++ lib/game/minesweeper/entity/record.dart | 5 +++-- lib/game/record/record.dart | 7 ++++++- .../electricity/service/electricity.demo.dart | 2 +- lib/life/expense_records/entity/local.dart | 6 +++++- lib/me/index.dart | 9 +++++---- lib/timetable/page/timetable.dart | 2 +- 8 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 lib/game/2048/entity/record.g.dart diff --git a/lib/game/2048/entity/record.dart b/lib/game/2048/entity/record.dart index 9065b704f..35b37e6cc 100644 --- a/lib/game/2048/entity/record.dart +++ b/lib/game/2048/entity/record.dart @@ -1,7 +1,13 @@ +import 'package:json_annotation/json_annotation.dart'; import 'package:sit/game/record/record.dart'; +part "record.g.dart"; + +@JsonSerializable() class Record2048 extends GameRecord { + @JsonKey() final int score; + @JsonKey() final int maxNumber; const Record2048({ diff --git a/lib/game/2048/entity/record.g.dart b/lib/game/2048/entity/record.g.dart new file mode 100644 index 000000000..1581b37e1 --- /dev/null +++ b/lib/game/2048/entity/record.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'record.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Record2048 _$Record2048FromJson(Map json) => Record2048( + ts: DateTime.parse(json['ts'] as String), + score: json['score'] as int, + maxNumber: json['maxNumber'] as int, + ); + +Map _$Record2048ToJson(Record2048 instance) => { + 'ts': instance.ts.toIso8601String(), + 'score': instance.score, + 'maxNumber': instance.maxNumber, + }; diff --git a/lib/game/minesweeper/entity/record.dart b/lib/game/minesweeper/entity/record.dart index d7796f504..7970982d1 100644 --- a/lib/game/minesweeper/entity/record.dart +++ b/lib/game/minesweeper/entity/record.dart @@ -1,6 +1,7 @@ import 'package:sit/game/record/record.dart'; class RecordMinesweeper extends GameRecord { - - const RecordMinesweeper({required super.ts}); + const RecordMinesweeper({ + required super.ts, + }); } diff --git a/lib/game/record/record.dart b/lib/game/record/record.dart index 625f53865..7a1ea547a 100644 --- a/lib/game/record/record.dart +++ b/lib/game/record/record.dart @@ -1,5 +1,10 @@ +import 'package:json_annotation/json_annotation.dart'; + class GameRecord { + @JsonKey() final DateTime ts; - const GameRecord({required this.ts}); + const GameRecord({ + required this.ts, + }); } diff --git a/lib/life/electricity/service/electricity.demo.dart b/lib/life/electricity/service/electricity.demo.dart index cb9c0b72d..aa4acb2c5 100644 --- a/lib/life/electricity/service/electricity.demo.dart +++ b/lib/life/electricity/service/electricity.demo.dart @@ -17,6 +17,6 @@ class DemoElectricityService implements ElectricityService { @override List getRoomNumberCandidates() { - return ["114514", "1919", "1314","6666"]; + return ["114514", "1919", "1314", "6666"]; } } diff --git a/lib/life/expense_records/entity/local.dart b/lib/life/expense_records/entity/local.dart index 52abd8145..449324eee 100644 --- a/lib/life/expense_records/entity/local.dart +++ b/lib/life/expense_records/entity/local.dart @@ -69,7 +69,11 @@ extension TransactionX on Transaction { (balanceAfter - balanceBefore) < 0 && type != TransactionType.topUp && type != TransactionType.subsidy; String? get bestTitle { - return deviceName.isNotEmpty? deviceName : note.isNotEmpty ? note : null; + return deviceName.isNotEmpty + ? deviceName + : note.isNotEmpty + ? note + : null; } String shortDeviceName() { diff --git a/lib/me/index.dart b/lib/me/index.dart index 22668f032..fe4cddf5d 100644 --- a/lib/me/index.dart +++ b/lib/me/index.dart @@ -23,6 +23,7 @@ const _qGroupNumber = "917740212"; const _joinQGroupUri = "mqqapi://card/show_pslcard?src_type=internal&version=1&uin=$_qGroupNumber&card_type=group&source=qrcode"; const _wechatUri = "weixin://dl/publicaccount?username=gh_61f7fd217d36"; + class MePage extends StatefulWidget { const MePage({super.key}); @@ -84,8 +85,8 @@ class _MePageState extends State { onPressed: () async { try { await launchUrlString(_joinQGroupUri); - } catch (error,stackTrace) { - debugPrintError(error,stackTrace); + } catch (error, stackTrace) { + debugPrintError(error, stackTrace); await Clipboard.setData(const ClipboardData(text: _qGroupNumber)); if (!mounted) return; context.showSnackBar(content: "已复制到剪贴板".text()); @@ -105,8 +106,8 @@ class _MePageState extends State { onPressed: () async { try { await launchUrlString(_wechatUri); - } catch (error,stackTrace) { - debugPrintError(error,stackTrace); + } catch (error, stackTrace) { + debugPrintError(error, stackTrace); } }, icon: Icon(context.icons.rightChevron), diff --git a/lib/timetable/page/timetable.dart b/lib/timetable/page/timetable.dart index f5bff87b2..36dc21655 100644 --- a/lib/timetable/page/timetable.dart +++ b/lib/timetable/page/timetable.dart @@ -113,7 +113,7 @@ class _TimetableBoardPageState extends State { Widget buildMyTimetablesButton() { return PlatformIconButton( - icon: Icon(context.icons.person, color:isCupertino? context.colorScheme.primary: null), + icon: Icon(context.icons.person, color: isCupertino ? context.colorScheme.primary : null), onPressed: () async { final focusMode = Settings.focusTimetable; if (focusMode) { From 027ff518e47edf248aa3dd39b1120c6fa83fdbef Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 12:45:04 +0800 Subject: [PATCH 021/458] [expense] floating header --- lib/l10n/time.dart | 10 ++++++++ lib/life/expense_records/page/statistics.dart | 24 +++++++++++++++---- lib/life/expense_records/widget/chart.dart | 19 ++++++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/l10n/time.dart b/lib/l10n/time.dart index ac33ead91..0a3950351 100644 --- a/lib/l10n/time.dart +++ b/lib/l10n/time.dart @@ -11,6 +11,16 @@ enum Weekday { int toJson() => index; + static const calendarOrder = [ + sunday, + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + ]; + int getIndex({required Weekday firstDay}) { return (this - firstDay.index).index; } diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index af29447bb..4035d01ec 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -24,7 +24,7 @@ class ExpenseStatisticsPage extends StatefulWidget { typedef Type2transactions = Map records, double total, double proportion})>; class _ExpenseStatisticsPageState extends State { - late List records; + late List allRecords; var selectedMode = StatisticsMode.month; late double total; @@ -35,8 +35,8 @@ class _ExpenseStatisticsPageState extends State { } void refreshRecords() { - records = ExpenseRecordsInit.storage.getTransactionsByRange() ?? const []; - records.retainWhere((record) => record.type.isConsume); + allRecords = ExpenseRecordsInit.storage.getTransactionsByRange() ?? const []; + allRecords.retainWhere((record) => record.type.isConsume); } Widget buildModeSelector() { @@ -67,7 +67,7 @@ class _ExpenseStatisticsPageState extends State { buildModeSelector().padSymmetric(h: 16, v: 4), StatisticsSection( mode: selectedMode, - all: records, + all: allRecords, ).expanded(), ].column(caa: CrossAxisAlignment.stretch), ); @@ -136,6 +136,22 @@ class _StatisticsSectionState extends State { Widget build(BuildContext context) { final current = startTime2Records.indexAt(index); final separated = separateTransactionByType(current.records); + return [ + [ + SizedBox(height: 50,), + ExpenseLineChart( + start: current.start, + records: current.records, + mode: widget.mode, + ), + ExpensePieChart(records: separated), + ...current.records.map((record) { + return TransactionTile(record); + }), + ].listview(), + // ListView() + buildHeader(current.start).align(at: Alignment.topCenter), + ].stack(); return [ buildHeader(current.start), [ diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index ae6942db0..a23512444 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -1,9 +1,11 @@ import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/l10n/time.dart'; import 'package:sit/utils/date.dart'; import '../entity/local.dart'; @@ -110,22 +112,24 @@ class ExpenseLineChart extends StatefulWidget { State createState() => _ExpenseLineChartState(); } +final _monthFormat = DateFormat.MMM(); + class _ExpenseLineChartState extends State { @override Widget build(BuildContext context) { - final data = buildData(); + final (:data, :titles) = buildData(); return OutlinedCard( child: AspectRatio( aspectRatio: 1.5, child: BaseLineChartWidget( - bottomTitles: List.generate(data.length, (i) => (i + 1).toString()), + bottomTitles: titles, values: data, ).padSymmetric(v: 12, h: 8), ), ); } - List buildData() { + ({List data, List titles}) buildData() { final now = DateTime.now(); final start = widget.start; switch (widget.mode) { @@ -136,7 +140,7 @@ class _ExpenseLineChartState extends State { // sunday goes first weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; } - return weekAmount; + return (data: weekAmount, titles: Weekday.calendarOrder.map((w) => w.l10nShort()).toList()); case StatisticsMode.month: final List dayAmount = List.filled( start.year == now.year && start.month == now.month @@ -147,14 +151,17 @@ class _ExpenseLineChartState extends State { // add data on the same day. dayAmount[record.timestamp.day - 1] += record.deltaAmount; } - return dayAmount; + return (data: dayAmount, titles: List.generate(dayAmount.length, (i) => (i + 1).toString())); case StatisticsMode.year: final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); for (final record in widget.records) { // add data in the same month. monthAmount[record.timestamp.month - 1] += record.deltaAmount; } - return monthAmount; + return ( + data: monthAmount, + titles: List.generate(monthAmount.length, (i) => _monthFormat.format(DateTime(0, i + 1)).substring(0,3)) + ); } } } From 4e17eb80c0f064fe870a52ac9c994ef74667c40a Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 13:04:49 +0800 Subject: [PATCH 022/458] [expense] [statistics] riverpod --- lib/life/expense_records/page/statistics.dart | 177 +++++++----------- 1 file changed, 67 insertions(+), 110 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 4035d01ec..69f60a105 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; @@ -14,135 +15,104 @@ import '../init.dart'; import '../widget/chart.dart'; import '../widget/transaction.dart'; -class ExpenseStatisticsPage extends StatefulWidget { +class ExpenseStatisticsPage extends ConsumerStatefulWidget { const ExpenseStatisticsPage({super.key}); @override - State createState() => _ExpenseStatisticsPageState(); + ConsumerState createState() => _ExpenseStatisticsPageState(); } typedef Type2transactions = Map records, double total, double proportion})>; -class _ExpenseStatisticsPageState extends State { - late List allRecords; - var selectedMode = StatisticsMode.month; - late double total; +final _allRecords = Provider.autoDispose((ref) { + final all = ExpenseRecordsInit.storage.getTransactionsByRange() ?? const []; + all.retainWhere((record) => record.type.isConsume); + return all; +}); +final _statisticsMode = +NotifierProvider.autoDispose<_StatisticsModeNotifier, StatisticsMode>(_StatisticsModeNotifier.new); + +class _StatisticsModeNotifier extends AutoDisposeNotifier { @override - void initState() { - super.initState(); - refreshRecords(); - } + StatisticsMode build() => StatisticsMode.month; - void refreshRecords() { - allRecords = ExpenseRecordsInit.storage.getTransactionsByRange() ?? const []; - allRecords.retainWhere((record) => record.type.isConsume); + void set(StatisticsMode mode) { + state = mode; } +} - Widget buildModeSelector() { - return SegmentedButton( - showSelectedIcon: false, - segments: StatisticsMode.values - .map((e) => ButtonSegment( - value: e, - label: e.l10nName().text(), - )) - .toList(), - selected: {selectedMode}, - onSelectionChanged: (newSelection) { - setState(() { - selectedMode = newSelection.first; - }); - }, - ); - } +final _startTime2Records = Provider.autoDispose((ref) { + final mode = ref.watch(_statisticsMode); + final all = ref.watch(_allRecords); + return mode.resort(all); +}); +class _ExpenseStatisticsPageState extends ConsumerState { @override Widget build(BuildContext context) { + final mode = ref.watch(_statisticsMode); return Scaffold( appBar: AppBar( title: i18n.stats.title.text(), ), body: [ - buildModeSelector().padSymmetric(h: 16, v: 4), - StatisticsSection( - mode: selectedMode, - all: allRecords, - ).expanded(), + buildModeSelector(mode).padSymmetric(h: 16, v: 4), + const StatisticsSection().expanded(), ].column(caa: CrossAxisAlignment.stretch), ); - // final now = DateTime.now(); - // final years = _getYear(records); - // final months = _getMonth(records, years, selectedYear); - // return Scaffold( - // body: CustomScrollView( - // slivers: [ - // SliverAppBar( - // pinned: true, - // expandedHeight: 200, - // flexibleSpace: FlexibleSpaceBar( - // title: i18n.stats.title.text(), - // centerTitle: true, - // background: YearMonthSelector( - // years: years, - // months: months, - // initialYear: now.year, - // initialMonth: now.month, - // ), - // ), - // ), - // SliverToBoxAdapter( - // child: _buildChartView(), - // ), - // SliverToBoxAdapter( - // child: ExpensePieChart(records: type2transactions), - // ), - // ], - // ), - // ); } -} -class StatisticsSection extends StatefulWidget { - final StatisticsMode mode; - final List all; + Widget buildModeSelector(StatisticsMode selected) { + return SegmentedButton( + showSelectedIcon: false, + segments: StatisticsMode.values + .map((e) => + ButtonSegment( + value: e, + label: e.l10nName().text(), + )) + .toList(), + selected: {selected}, + onSelectionChanged: (newSelection) { + ref.read(_statisticsMode.notifier).set(newSelection.first); + }, + ); + } +} +class StatisticsSection extends ConsumerStatefulWidget { const StatisticsSection({ super.key, - required this.mode, - required this.all, }); @override - State createState() => _StatisticsSectionState(); + ConsumerState createState() => _StatisticsSectionState(); } -class _StatisticsSectionState extends State { - late StartTime2Records startTime2Records = widget.mode.resort(widget.all); - late int index = startTime2Records.length - 1; +class _StatisticsSectionState extends ConsumerState { + late int index = ref.read(_startTime2Records).length - 1; @override - void didUpdateWidget(covariant StatisticsSection oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.mode != widget.mode || oldWidget.all != widget.all) { + Widget build(BuildContext context) { + final startTime2Records = ref.watch(_startTime2Records); + final mode = ref.watch(_statisticsMode); + ref.listen(_startTime2Records, (previous, next) { setState(() { - startTime2Records = widget.mode.resort(widget.all); - index = startTime2Records.length - 1; + index = next.length - 1; }); - } - } - - @override - Widget build(BuildContext context) { + }); final current = startTime2Records.indexAt(index); final separated = separateTransactionByType(current.records); return [ [ - SizedBox(height: 50,), + SizedBox( + height: 50, + ), ExpenseLineChart( start: current.start, records: current.records, - mode: widget.mode, + mode: mode, ), ExpensePieChart(records: separated), ...current.records.map((record) { @@ -152,44 +122,31 @@ class _StatisticsSectionState extends State { // ListView() buildHeader(current.start).align(at: Alignment.topCenter), ].stack(); - return [ - buildHeader(current.start), - [ - ExpenseLineChart( - start: current.start, - records: current.records, - mode: widget.mode, - ), - ExpensePieChart(records: separated), - ...current.records.map((record) { - return TransactionTile(record); - }), - ].listview().expanded(), - // ListView() - ].column(); } Widget buildHeader(DateTime start) { + final startTime2Records = ref.watch(_startTime2Records); + final mode = ref.watch(_statisticsMode); return OutlinedCard( child: [ PlatformIconButton( onPressed: index > 0 ? () { - setState(() { - index = index - 1; - }); - } + setState(() { + index = index - 1; + }); + } : null, icon: Icon(context.icons.leftChevron), ), - resolveTime4Display(context: context, mode: widget.mode, date: start).text(), + resolveTime4Display(context: context, mode: mode, date: start).text(), PlatformIconButton( onPressed: index < startTime2Records.length - 1 ? () { - setState(() { - index = index + 1; - }); - } + setState(() { + index = index + 1; + }); + } : null, icon: Icon(context.icons.rightChevron), ), From bf8d6db3253ea173279ebfc68a10f90583cf5b76 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 13:22:37 +0800 Subject: [PATCH 023/458] [expense] [statistics] it looks like Screen Time --- lib/design/widgets/fab.dart | 2 +- lib/life/expense_records/page/statistics.dart | 145 ++++++++++-------- 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/lib/design/widgets/fab.dart b/lib/design/widgets/fab.dart index 1e3359a3a..d1628aab3 100644 --- a/lib/design/widgets/fab.dart +++ b/lib/design/widgets/fab.dart @@ -54,7 +54,7 @@ class _AutoHideFABState extends State { @override void dispose() { - widget.controller.addListener(onScrollChanged); + widget.controller.removeListener(onScrollChanged); super.dispose(); } diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 69f60a105..80153d250 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,4 +1,7 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; @@ -31,7 +34,7 @@ final _allRecords = Provider.autoDispose((ref) { }); final _statisticsMode = -NotifierProvider.autoDispose<_StatisticsModeNotifier, StatisticsMode>(_StatisticsModeNotifier.new); + NotifierProvider.autoDispose<_StatisticsModeNotifier, StatisticsMode>(_StatisticsModeNotifier.new); class _StatisticsModeNotifier extends AutoDisposeNotifier { @override @@ -49,17 +52,80 @@ final _startTime2Records = Provider.autoDispose((ref) { }); class _ExpenseStatisticsPageState extends ConsumerState { + late int index = ref.read(_startTime2Records).length - 1; + final controller = ScrollController(); + var showTimeSpan = false; + + @override + void initState() { + super.initState(); + controller.addListener(() { + final pos = controller.positions.last; + final direction = pos.userScrollDirection; + if (direction == ScrollDirection.reverse) { + if (!showTimeSpan) { + setState(() { + showTimeSpan = true; + }); + } + } + if (pos.pixels <= pos.minScrollExtent) { + if (showTimeSpan) { + setState(() { + showTimeSpan = false; + }); + } + } + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final mode = ref.watch(_statisticsMode); + final startTime2Records = ref.watch(_startTime2Records); + ref.listen(_startTime2Records, (previous, next) { + setState(() { + index = next.length - 1; + }); + }); + final current = startTime2Records.indexAt(index); + final separated = separateTransactionByType(current.records); return Scaffold( appBar: AppBar( title: i18n.stats.title.text(), ), body: [ - buildModeSelector(mode).padSymmetric(h: 16, v: 4), - const StatisticsSection().expanded(), - ].column(caa: CrossAxisAlignment.stretch), + ListView( + controller: controller, + children: [ + buildModeSelector(mode).padSymmetric(h: 16, v: 4), + ExpenseLineChart( + start: current.start, + records: current.records, + mode: mode, + ), + ExpensePieChart(records: separated), + ...current.records.map((record) { + return TransactionTile(record); + }), + ], + ), + // ListView() + AnimatedSlide( + offset: showTimeSpan ? Offset.zero : const Offset(0, -2), + duration: Durations.long4, + child: AnimatedSwitcher( + duration: Durations.long4, + child: showTimeSpan ? buildHeader(current.start) : null, + ), + ).align(at: Alignment.topCenter), + ].stack(), ); } @@ -67,11 +133,10 @@ class _ExpenseStatisticsPageState extends ConsumerState { return SegmentedButton( showSelectedIcon: false, segments: StatisticsMode.values - .map((e) => - ButtonSegment( - value: e, - label: e.l10nName().text(), - )) + .map((e) => ButtonSegment( + value: e, + label: e.l10nName().text(), + )) .toList(), selected: {selected}, onSelectionChanged: (newSelection) { @@ -79,63 +144,19 @@ class _ExpenseStatisticsPageState extends ConsumerState { }, ); } -} - -class StatisticsSection extends ConsumerStatefulWidget { - const StatisticsSection({ - super.key, - }); - - @override - ConsumerState createState() => _StatisticsSectionState(); -} - -class _StatisticsSectionState extends ConsumerState { - late int index = ref.read(_startTime2Records).length - 1; - - @override - Widget build(BuildContext context) { - final startTime2Records = ref.watch(_startTime2Records); - final mode = ref.watch(_statisticsMode); - ref.listen(_startTime2Records, (previous, next) { - setState(() { - index = next.length - 1; - }); - }); - final current = startTime2Records.indexAt(index); - final separated = separateTransactionByType(current.records); - return [ - [ - SizedBox( - height: 50, - ), - ExpenseLineChart( - start: current.start, - records: current.records, - mode: mode, - ), - ExpensePieChart(records: separated), - ...current.records.map((record) { - return TransactionTile(record); - }), - ].listview(), - // ListView() - buildHeader(current.start).align(at: Alignment.topCenter), - ].stack(); - } Widget buildHeader(DateTime start) { final startTime2Records = ref.watch(_startTime2Records); final mode = ref.watch(_statisticsMode); - return OutlinedCard( + return FilledCard( child: [ PlatformIconButton( onPressed: index > 0 ? () { - setState(() { - index = index - 1; - }); - } + setState(() { + index = index - 1; + }); + } : null, icon: Icon(context.icons.leftChevron), ), @@ -143,10 +164,10 @@ class _StatisticsSectionState extends ConsumerState { PlatformIconButton( onPressed: index < startTime2Records.length - 1 ? () { - setState(() { - index = index + 1; - }); - } + setState(() { + index = index + 1; + }); + } : null, icon: Icon(context.icons.rightChevron), ), From 0af50c941d5076c82199ba35af84a424a47f577e Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 3 Apr 2024 19:01:18 +0800 Subject: [PATCH 024/458] [expense] [statistics] auto-display span header --- lib/life/expense_records/page/statistics.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 80153d250..8602aaf88 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -61,15 +61,13 @@ class _ExpenseStatisticsPageState extends ConsumerState { super.initState(); controller.addListener(() { final pos = controller.positions.last; - final direction = pos.userScrollDirection; - if (direction == ScrollDirection.reverse) { + if (pos.pixels > pos.minScrollExtent) { if (!showTimeSpan) { setState(() { showTimeSpan = true; }); } - } - if (pos.pixels <= pos.minScrollExtent) { + } else { if (showTimeSpan) { setState(() { showTimeSpan = false; From 17848c4b29ca64ecd02bacbc77e53ae5e327ec48 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 4 Apr 2024 01:19:40 +0800 Subject: [PATCH 025/458] show action request --- lib/school/class2nd/page/attended.dart | 7 +++---- lib/school/class2nd/page/details.dart | 14 ++++++-------- lib/school/class2nd/service/points.dart | 4 ++++ lib/settings/page/index.dart | 14 ++++++-------- lib/settings/page/storage.dart | 14 ++++++++++++-- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/school/class2nd/page/attended.dart b/lib/school/class2nd/page/attended.dart index b1d5a0133..53edafeca 100644 --- a/lib/school/class2nd/page/attended.dart +++ b/lib/school/class2nd/page/attended.dart @@ -358,11 +358,10 @@ class _Class2ndAttendDetailsPageState extends State { } Future withdrawApplication() async { - final confirm = await context.showDialogRequest( - title: i18n.attended.withdrawApplication, + final confirm = await context.showActionRequest( + action: i18n.attended.withdrawApplication, desc: i18n.attended.withdrawApplicationDesc, - yes: i18n.yes, - no: i18n.cancel, + cancel: i18n.cancel, ); if (confirm != true) return; setState(() { diff --git a/lib/school/class2nd/page/details.dart b/lib/school/class2nd/page/details.dart index c51facea6..3b2758eb0 100644 --- a/lib/school/class2nd/page/details.dart +++ b/lib/school/class2nd/page/details.dart @@ -146,11 +146,10 @@ class _Class2ndActivityDetailsPageState extends State showApplyRequest() async { - final confirm = await context.showDialogRequest( - title: i18n.apply.applyRequest, + final confirm = await context.showActionRequest( + action: i18n.apply.applyRequest, desc: i18n.apply.applyRequestDesc, - yes: i18n.confirm, - no: i18n.cancel, + cancel: i18n.cancel, destructive: true, ); if (confirm != true) return; @@ -174,11 +173,10 @@ class _Class2ndActivityDetailsPageState extends State showForciblyApplyRequest() async { - final confirm = await context.showDialogRequest( - title: "Forcibly apply", + final confirm = await context.showActionRequest( + action: "Forcibly apply", desc: "Confirm to apply this activity forcibly?", - yes: i18n.confirm, - no: i18n.cancel, + cancel: i18n.cancel, destructive: true, ); if (confirm != true) return; diff --git a/lib/school/class2nd/service/points.dart b/lib/school/class2nd/service/points.dart index d33699b36..ee569316d 100644 --- a/lib/school/class2nd/service/points.dart +++ b/lib/school/class2nd/service/points.dart @@ -1,5 +1,6 @@ import 'package:beautiful_soup_dart/beautiful_soup.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:sit/init.dart'; @@ -61,6 +62,9 @@ class Class2ndPointsService { }) _parseAllStatus(BeautifulSoup html) { // 学分=1.5(主题报告)+2.0(社会实践)+1.5(创新创业创意)+1.0(校园安全文明)+0.0(公益志愿)+2.0(校园文化) final found = html.find('#span_score'); + if(found == null){ + debugPrint(found.toString()); + } final String scoreText = found!.text; final regExp = RegExp(r'([\d.]+)\(([\u4e00-\u9fa5]+)\)'); diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index 6e0bbb779..29de27653 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -200,11 +200,10 @@ class ClearCacheTile extends StatelessWidget { } void _onClearCache(BuildContext context) async { - final confirm = await context.showDialogRequest( - title: i18n.clearCacheTitle, + final confirm = await context.showActionRequest( + action: i18n.clearCacheTitle, desc: i18n.clearCacheRequest, - yes: i18n.confirm, - no: i18n.cancel, + cancel: i18n.cancel, destructive: true, ); if (confirm == true) { @@ -229,11 +228,10 @@ class WipeDataTile extends StatelessWidget { } Future _onWipeData(BuildContext context) async { - final confirm = await context.showDialogRequest( - title: i18n.wipeDataRequest, + final confirm = await context.showActionRequest( + action: i18n.wipeDataRequest, desc: i18n.wipeDataRequestDesc, - yes: i18n.confirm, - no: i18n.cancel, + cancel: i18n.cancel, destructive: true, ); if (confirm == true) { diff --git a/lib/settings/page/storage.dart b/lib/settings/page/storage.dart index 926bc7909..37065d24e 100644 --- a/lib/settings/page/storage.dart +++ b/lib/settings/page/storage.dart @@ -453,10 +453,20 @@ dynamic _emptyValue(dynamic value) { Future _showDeleteBoxRequest(BuildContext ctx) async { return await ctx.showDialogRequest( - title: i18n.delete, desc: i18n.dev.storage.clearBoxDesc, yes: i18n.confirm, no: i18n.cancel, destructive: true); + title: i18n.delete, + desc: i18n.dev.storage.clearBoxDesc, + yes: i18n.confirm, + no: i18n.cancel, + destructive: true, + ); } Future _showDeleteItemRequest(BuildContext ctx) async { return await ctx.showDialogRequest( - title: i18n.delete, desc: i18n.dev.storage.deleteItemDesc, yes: i18n.delete, no: i18n.cancel, destructive: true); + title: i18n.delete, + desc: i18n.dev.storage.deleteItemDesc, + yes: i18n.delete, + no: i18n.cancel, + destructive: true, + ); } From 2e22e950c4da509724971a5cd8dfd21667dce9ba Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 4 Apr 2024 02:09:55 +0800 Subject: [PATCH 026/458] [expense] [statistics] hide unavailable column in line chart --- lib/life/expense_records/widget/chart.dart | 80 ++++++++++++---------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index a23512444..0a0291b15 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -117,7 +117,11 @@ final _monthFormat = DateFormat.MMM(); class _ExpenseLineChartState extends State { @override Widget build(BuildContext context) { - final (:data, :titles) = buildData(); + final (:data, :titles) = buildData( + start: widget.start, + mode: widget.mode, + records: widget.records, + ); return OutlinedCard( child: AspectRatio( aspectRatio: 1.5, @@ -128,41 +132,47 @@ class _ExpenseLineChartState extends State { ), ); } +} - ({List data, List titles}) buildData() { - final now = DateTime.now(); - final start = widget.start; - switch (widget.mode) { - case StatisticsMode.week: - final List weekAmount = List.filled(7, 0.00); - for (final record in widget.records) { - // add data at the same weekday. - // sunday goes first - weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; - } - return (data: weekAmount, titles: Weekday.calendarOrder.map((w) => w.l10nShort()).toList()); - case StatisticsMode.month: - final List dayAmount = List.filled( - start.year == now.year && start.month == now.month - ? now.day - : daysInMonth(year: start.year, month: start.month), - 0.00); - for (final record in widget.records) { - // add data on the same day. - dayAmount[record.timestamp.day - 1] += record.deltaAmount; - } - return (data: dayAmount, titles: List.generate(dayAmount.length, (i) => (i + 1).toString())); - case StatisticsMode.year: - final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); - for (final record in widget.records) { - // add data in the same month. - monthAmount[record.timestamp.month - 1] += record.deltaAmount; - } - return ( - data: monthAmount, - titles: List.generate(monthAmount.length, (i) => _monthFormat.format(DateTime(0, i + 1)).substring(0,3)) - ); - } +({List data, List titles}) buildData({ + required DateTime start, + required StatisticsMode mode, + required List records, +}) { + final now = DateTime.now(); + switch (mode) { + case StatisticsMode.week: + final List weekAmount = List.filled( + start.year == now.year && start.week == now.week ? now.weekday : 7, + 0.00, + ); + for (final record in records) { + // add data at the same weekday. + // sunday goes first + weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; + } + return (data: weekAmount, titles: Weekday.calendarOrder.map((w) => w.l10nShort()).toList()); + case StatisticsMode.month: + final List dayAmount = List.filled( + start.year == now.year && start.month == now.month + ? now.day + : daysInMonth(year: start.year, month: start.month), + 0.00); + for (final record in records) { + // add data on the same day. + dayAmount[record.timestamp.day - 1] += record.deltaAmount; + } + return (data: dayAmount, titles: List.generate(dayAmount.length, (i) => (i + 1).toString())); + case StatisticsMode.year: + final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); + for (final record in records) { + // add data in the same month. + monthAmount[record.timestamp.month - 1] += record.deltaAmount; + } + return ( + data: monthAmount, + titles: List.generate(monthAmount.length, (i) => _monthFormat.format(DateTime(0, i + 1)).substring(0, 3)) + ); } } From 5e8cc4c26d0b8a80a4fbb84a7aa0a07b1d6e7ba3 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 5 Apr 2024 02:42:01 +0800 Subject: [PATCH 027/458] [expense] [statistics] using bar chart --- lib/life/expense_records/widget/chart.dart | 121 +++++++++------------ 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart index 0a0291b15..ade7dccc8 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart.dart @@ -125,7 +125,7 @@ class _ExpenseLineChartState extends State { return OutlinedCard( child: AspectRatio( aspectRatio: 1.5, - child: BaseLineChartWidget( + child: AmountChartWidget( bottomTitles: titles, values: data, ).padSymmetric(v: 12, h: 8), @@ -176,103 +176,90 @@ class _ExpenseLineChartState extends State { } } -class BaseLineChartWidget extends StatelessWidget { +class AmountChartWidget extends StatelessWidget { final List bottomTitles; final List values; - const BaseLineChartWidget({ + const AmountChartWidget({ super.key, required this.bottomTitles, required this.values, }); - ///底部标题栏 - Widget bottomTitle(BuildContext ctx, double value, TitleMeta mate) { - if ((value * 10).toInt() % 10 == 5) { + Widget buildLeftTitle(double value, TitleMeta meta) { + String text = '¥${value.round()}'; + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text(text), + ); + } + + Widget buildBottomTitle(double value, TitleMeta mate) { + final index = value.toInt(); + if (!(index == 0 || index == values.length - 1) && index % 5 != 0) { return const SizedBox(); } return SideTitleWidget( axisSide: mate.axisSide, child: Text( - bottomTitles[value.toInt()], - style: ctx.textTheme.bodySmall?.copyWith( - color: Colors.blueGrey, - ), + bottomTitles[index], ), ); } - ///左边部标题栏 - Widget leftTitle(BuildContext ctx, double value, TitleMeta mate) { - const style = TextStyle( - color: Colors.blueGrey, - fontSize: 11, - ); - String text = '¥${value.toStringAsFixed(2)}'; - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text(text, style: style), - ); - } - @override Widget build(BuildContext context) { - return LineChart( - LineChartData( - ///触摸控制 - lineTouchData: LineTouchData( - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (touchedSpot) => Colors.transparent, - ), - touchSpotThreshold: 10, - ), - borderData: FlBorderData( - border: const Border( - bottom: BorderSide.none, - ), - ), - lineBarsData: [ - LineChartBarData( - isStrokeCapRound: true, - belowBarData: BarAreaData( - show: true, - color: context.colorScheme.primary.withOpacity(0.15), - ), - spots: values - .map((e) => (e * 100).toInt() / 100) // 保留两位小数 - .toList() - .asMap() - .entries - .map((e) => FlSpot(e.key.toDouble(), e.value)) - .toList(), - color: context.colorScheme.primary, - isCurved: true, - preventCurveOverShooting: true, - barWidth: 1, - ), - ], - - ///图表线表线框 + return BarChart( + BarChartData( + alignment: BarChartAlignment.center, + barTouchData: BarTouchData(enabled: false), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles(), - leftTitles: AxisTitles( + bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 50, - getTitlesWidget: (v, meta) => leftTitle(context, v, meta), + reservedSize: 28, + getTitlesWidget: buildBottomTitle, ), ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( + leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 55, - getTitlesWidget: (v, meta) => bottomTitle(context, v, meta), + reservedSize: 40, + getTitlesWidget: buildLeftTitle, ), ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + checkToShowHorizontalLine: (value) => value % 5 == 0, + getDrawingHorizontalLine: (value) => FlLine( + color: context.colorScheme.secondary.withOpacity(0.2), + strokeWidth: 1, + ), + drawVerticalLine: false, + ), + borderData: FlBorderData( + show: false, ), + groupsSpace: 40, + barGroups: values + .mapIndexed((i, v) => BarChartGroupData( + x: i, + barRods: [ + BarChartRodData( + toY: v, + ), + ], + )) + .toList(), ), ); } From 6de75dfaac62aa8084558efbf93a8e3e615e53c4 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 5 Apr 2024 02:44:07 +0800 Subject: [PATCH 028/458] [expense] [statistics] separate bar and pie chart --- lib/life/expense_records/page/statistics.dart | 3 +- .../widget/{chart.dart => chart/bar.dart} | 90 +----------------- .../expense_records/widget/chart/pie.dart | 94 +++++++++++++++++++ 3 files changed, 99 insertions(+), 88 deletions(-) rename lib/life/expense_records/widget/{chart.dart => chart/bar.dart} (65%) create mode 100644 lib/life/expense_records/widget/chart/pie.dart diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 8602aaf88..af4238cee 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -15,7 +15,8 @@ import 'package:sit/utils/collection.dart'; import '../entity/local.dart'; import '../i18n.dart'; import '../init.dart'; -import '../widget/chart.dart'; +import '../widget/chart/bar.dart'; +import '../widget/chart/pie.dart'; import '../widget/transaction.dart'; class ExpenseStatisticsPage extends ConsumerStatefulWidget { diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart/bar.dart similarity index 65% rename from lib/life/expense_records/widget/chart.dart rename to lib/life/expense_records/widget/chart/bar.dart index ade7dccc8..ea2817f12 100644 --- a/lib/life/expense_records/widget/chart.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -8,93 +8,9 @@ import 'package:rettulf/rettulf.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/utils/date.dart'; -import '../entity/local.dart'; -import '../entity/statistics.dart'; -import "../i18n.dart"; - -class ExpensePieChart extends StatefulWidget { - final Map records, double total, double proportion})> records; - - const ExpensePieChart({ - super.key, - required this.records, - }); - - @override - State createState() => _ExpensePieChartState(); -} - -class _ExpensePieChartState extends State { - int touchedIndex = -1; - - @override - Widget build(BuildContext context) { - assert(widget.records.keys.every((type) => type.isConsume)); - return OutlinedCard( - child: [ - AspectRatio( - aspectRatio: 1.5, - child: buildChart(), - ), - buildLegends().padAll(8).align(at: Alignment.topLeft), - ].column(), - ); - } - - Widget buildChart() { - return PieChart( - PieChartData( - pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, pieTouchResponse) { - setState(() { - if (!event.isInterestedForInteractions || - pieTouchResponse == null || - pieTouchResponse.touchedSection == null) { - touchedIndex = -1; - return; - } - touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; - }); - }, - ), - borderData: FlBorderData( - show: false, - ), - sectionsSpace: 0, - centerSpaceRadius: 60, - sections: widget.records.entries.mapIndexed((i, entry) { - final isTouched = i == touchedIndex; - final MapEntry(key: type, value: (records: _, :total, :proportion)) = entry; - final color = type.color.harmonizeWith(context.colorScheme.primary); - return PieChartSectionData( - color: color.withOpacity(isTouched ? 1 : 0.8), - value: total, - title: "${(proportion * 100).toStringAsFixed(2)}%", - titleStyle: context.textTheme.titleSmall, - radius: isTouched ? 55 : 50, - badgeWidget: Icon(type.icon, color: color), - badgePositionPercentageOffset: 1.5, - ); - }).toList(), - ), - ); - } - - Widget buildLegends() { - return widget.records.entries - .map((record) { - final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; - final color = type.color.harmonizeWith(context.colorScheme.primary); - return Chip( - avatar: Icon(type.icon, color: color), - labelStyle: TextStyle(color: color), - label: "${type.localized()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), - ); - }) - .toList() - .wrap(spacing: 4); - } -} +import '../../entity/local.dart'; +import '../../entity/statistics.dart'; +import "../../i18n.dart"; class ExpenseLineChart extends StatefulWidget { final DateTime start; diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart new file mode 100644 index 000000000..b946b157c --- /dev/null +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -0,0 +1,94 @@ +import 'package:collection/collection.dart'; +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rettulf/rettulf.dart'; +import 'package:sit/design/widgets/card.dart'; + +import '../../entity/local.dart'; +import "../../i18n.dart"; + +class ExpensePieChart extends StatefulWidget { + final Map records, double total, double proportion})> records; + + const ExpensePieChart({ + super.key, + required this.records, + }); + + @override + State createState() => _ExpensePieChartState(); +} + +class _ExpensePieChartState extends State { + int touchedIndex = -1; + + @override + Widget build(BuildContext context) { + assert(widget.records.keys.every((type) => type.isConsume)); + return OutlinedCard( + child: [ + AspectRatio( + aspectRatio: 1.5, + child: buildChart(), + ), + buildLegends().padAll(8).align(at: Alignment.topLeft), + ].column(), + ); + } + + Widget buildChart() { + return PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; + }); + }, + ), + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 0, + centerSpaceRadius: 60, + sections: widget.records.entries.mapIndexed((i, entry) { + final isTouched = i == touchedIndex; + final MapEntry(key: type, value: (records: _, :total, :proportion)) = entry; + final color = type.color.harmonizeWith(context.colorScheme.primary); + return PieChartSectionData( + color: color.withOpacity(isTouched ? 1 : 0.8), + value: total, + title: "${(proportion * 100).toStringAsFixed(2)}%", + titleStyle: context.textTheme.titleSmall, + radius: isTouched ? 55 : 50, + badgeWidget: Icon(type.icon, color: color), + badgePositionPercentageOffset: 1.5, + ); + }).toList(), + ), + ); + } + + Widget buildLegends() { + return widget.records.entries + .map((record) { + final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; + final color = type.color.harmonizeWith(context.colorScheme.primary); + return Chip( + avatar: Icon(type.icon, color: color), + labelStyle: TextStyle(color: color), + label: "${type.localized()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), + ); + }) + .toList() + .wrap(spacing: 4); + } +} From b66644bf3bf5820dd8615e3461ce218496eb9672 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 5 Apr 2024 03:51:50 +0800 Subject: [PATCH 029/458] [expense] [statistics] StatisticsDelegate --- .../expense_records/widget/chart/bar.dart | 196 +++++++++++------- 1 file changed, 126 insertions(+), 70 deletions(-) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index ea2817f12..96ae694d6 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -28,101 +28,157 @@ class ExpenseLineChart extends StatefulWidget { State createState() => _ExpenseLineChartState(); } -final _monthFormat = DateFormat.MMM(); - class _ExpenseLineChartState extends State { @override Widget build(BuildContext context) { - final (:data, :titles) = buildData( + final delegate = StatisticsDelegate.byMode( + widget.mode, start: widget.start, - mode: widget.mode, records: widget.records, ); return OutlinedCard( child: AspectRatio( aspectRatio: 1.5, child: AmountChartWidget( - bottomTitles: titles, - values: data, + delegate: delegate, ).padSymmetric(v: 12, h: 8), ), ); } } -({List data, List titles}) buildData({ - required DateTime start, - required StatisticsMode mode, - required List records, -}) { - final now = DateTime.now(); - switch (mode) { - case StatisticsMode.week: - final List weekAmount = List.filled( - start.year == now.year && start.week == now.week ? now.weekday : 7, - 0.00, - ); - for (final record in records) { - // add data at the same weekday. - // sunday goes first - weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; - } - return (data: weekAmount, titles: Weekday.calendarOrder.map((w) => w.l10nShort()).toList()); - case StatisticsMode.month: - final List dayAmount = List.filled( - start.year == now.year && start.month == now.month - ? now.day - : daysInMonth(year: start.year, month: start.month), - 0.00); - for (final record in records) { - // add data on the same day. - dayAmount[record.timestamp.day - 1] += record.deltaAmount; - } - return (data: dayAmount, titles: List.generate(dayAmount.length, (i) => (i + 1).toString())); - case StatisticsMode.year: - final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); - for (final record in records) { - // add data in the same month. - monthAmount[record.timestamp.month - 1] += record.deltaAmount; - } - return ( - data: monthAmount, - titles: List.generate(monthAmount.length, (i) => _monthFormat.format(DateTime(0, i + 1)).substring(0, 3)) - ); - } -} +final _monthFormat = DateFormat.MMM(); -class AmountChartWidget extends StatelessWidget { - final List bottomTitles; - final List values; +class StatisticsDelegate { + final List data; + final StatisticsMode mode; + final GetTitleWidgetFunction side; + final GetTitleWidgetFunction bottom; - const AmountChartWidget({ - super.key, - required this.bottomTitles, - required this.values, + const StatisticsDelegate({ + required this.mode, + required this.data, + required this.side, + required this.bottom, }); - Widget buildLeftTitle(double value, TitleMeta meta) { - String text = '¥${value.round()}'; - return SideTitleWidget( - axisSide: meta.axisSide, - child: Text(text), + factory StatisticsDelegate.byMode( + StatisticsMode mode, { + required DateTime start, + required List records, + }) { + switch (mode) { + case StatisticsMode.week: + return StatisticsDelegate.week(start: start, records: records); + case StatisticsMode.month: + return StatisticsDelegate.month(start: start, records: records); + case StatisticsMode.year: + return StatisticsDelegate.year(start: start, records: records); + } + } + + factory StatisticsDelegate.week({ + required DateTime start, + required List records, + }) { + final now = DateTime.now(); + final List weekAmount = List.filled( + start.year == now.year && start.week == now.week ? now.weekday : 7, + 0.00, + ); + for (final record in records) { + // add data at the same weekday. + // sunday goes first + weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; + } + return StatisticsDelegate( + mode: StatisticsMode.week, + data: weekAmount, + side: _buildSideTitle, + bottom: (value, mate) { + final index = value.toInt(); + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + Weekday.calendarOrder[index].l10nShort(), + ), + ); + }, ); } - Widget buildBottomTitle(double value, TitleMeta mate) { - final index = value.toInt(); - if (!(index == 0 || index == values.length - 1) && index % 5 != 0) { - return const SizedBox(); + factory StatisticsDelegate.month({ + required DateTime start, + required List records, + }) { + final now = DateTime.now(); + final List dayAmount = List.filled( + start.year == now.year && start.month == now.month + ? now.day + : daysInMonth(year: start.year, month: start.month), + 0.00); + for (final record in records) { + // add data on the same day. + dayAmount[record.timestamp.day - 1] += record.deltaAmount; } + return StatisticsDelegate( + mode: StatisticsMode.week, + data: dayAmount, + side: _buildSideTitle, + bottom: (value, mate) { + final index = value.toInt(); + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + "${index + 1}", + ), + ); + }, + ); + } + + factory StatisticsDelegate.year({ + required DateTime start, + required List records, + }) { + final now = DateTime.now(); + final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); + for (final record in records) { + // add data in the same month. + monthAmount[record.timestamp.month - 1] += record.deltaAmount; + } + return StatisticsDelegate( + mode: StatisticsMode.week, + data: monthAmount, + side: _buildSideTitle, + bottom: (value, mate) { + final index = value.toInt(); + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + _monthFormat.format(DateTime(0, index + 1)).substring(0, 3), + ), + ); + }, + ); + } + static Widget _buildSideTitle(double value, TitleMeta meta) { + String text = '¥${value.round()}'; return SideTitleWidget( - axisSide: mate.axisSide, - child: Text( - bottomTitles[index], - ), + axisSide: meta.axisSide, + child: Text(text), ); } +} + +class AmountChartWidget extends StatelessWidget { + final StatisticsDelegate delegate; + + const AmountChartWidget({ + super.key, + required this.delegate, + }); @override Widget build(BuildContext context) { @@ -136,14 +192,14 @@ class AmountChartWidget extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 28, - getTitlesWidget: buildBottomTitle, + getTitlesWidget: delegate.bottom, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, - getTitlesWidget: buildLeftTitle, + getTitlesWidget: delegate.side, ), ), topTitles: const AxisTitles( @@ -166,7 +222,7 @@ class AmountChartWidget extends StatelessWidget { show: false, ), groupsSpace: 40, - barGroups: values + barGroups: delegate.data .mapIndexed((i, v) => BarChartGroupData( x: i, barRods: [ From 367119c703e9d26be9dd5c257aa5d346c3b73c33 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sat, 6 Apr 2024 00:56:25 +0800 Subject: [PATCH 030/458] [timetable] inspect issues --- lib/timetable/entity/patch.dart | 24 +++-- lib/timetable/entity/pos.dart | 16 +++ lib/timetable/entity/timetable.dart | 104 +++++++++++++++++++- lib/timetable/utils.dart | 49 --------- lib/timetable/widgets/timetable/weekly.dart | 2 +- 5 files changed, 132 insertions(+), 63 deletions(-) diff --git a/lib/timetable/entity/patch.dart b/lib/timetable/entity/patch.dart index e5a300277..66081bbaa 100644 --- a/lib/timetable/entity/patch.dart +++ b/lib/timetable/entity/patch.dart @@ -4,43 +4,47 @@ enum TimetablePatchType { moveLesson, moveDay, addLesson, + swipeLesson, + swipeDay, + replaceLesson, + replaceDay, } abstract class TimetablePatch { TimetablePatchType get type; } -class RemoveLessonTimetablePatch implements TimetablePatch { +class TimetableRemoveLessonPatch implements TimetablePatch { @override final type = TimetablePatchType.removeLesson; - const RemoveLessonTimetablePatch(); + const TimetableRemoveLessonPatch(); } -class RemoveDayTimetablePatch implements TimetablePatch { +class TimetableRemoveDayPatch implements TimetablePatch { @override final type = TimetablePatchType.removeDay; - const RemoveDayTimetablePatch(); + const TimetableRemoveDayPatch(); } -class MoveLessonTimetablePatch implements TimetablePatch { +class TimetableMoveLessonPatch implements TimetablePatch { @override final type = TimetablePatchType.moveLesson; - const MoveLessonTimetablePatch(); + const TimetableMoveLessonPatch(); } -class MoveDayTimetablePatch implements TimetablePatch { +class TimetableMoveDayPatch implements TimetablePatch { @override final type = TimetablePatchType.moveDay; - const MoveDayTimetablePatch(); + const TimetableMoveDayPatch(); } -class AddLessonTimetablePatch implements TimetablePatch { +class TimetableAddLessonPatch implements TimetablePatch { @override final type = TimetablePatchType.addLesson; - const AddLessonTimetablePatch(); + const TimetableAddLessonPatch(); } diff --git a/lib/timetable/entity/pos.dart b/lib/timetable/entity/pos.dart index 53e4d4da6..3e24ad582 100644 --- a/lib/timetable/entity/pos.dart +++ b/lib/timetable/entity/pos.dart @@ -66,3 +66,19 @@ extension TimetableX on SitTimetable { return TimetablePos.locate(current, relativeTo: startDate); } } + +class TimetableLessonLoc { + final String courseCode; + + /// starts with 0 + final int weekIndex; + + /// starts with 0 + final Weekday weekday; + + const TimetableLessonLoc({ + required this.courseCode, + required this.weekIndex, + required this.weekday, + }); +} diff --git a/lib/timetable/entity/timetable.dart b/lib/timetable/entity/timetable.dart index bc2924577..457da0fef 100644 --- a/lib/timetable/entity/timetable.dart +++ b/lib/timetable/entity/timetable.dart @@ -5,6 +5,7 @@ import 'package:sit/entity/campus.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/school/entity/school.dart'; import 'package:sit/school/entity/timetable.dart'; +import 'package:sit/school/utils.dart'; import 'package:sit/timetable/entity/platte.dart'; import '../utils.dart'; @@ -45,9 +46,7 @@ class SitTimetable { this.version = 1, }); - SitTimetableEntity resolve() { - return resolveTimetableEntity(this); - } + SitTimetableEntity resolve() => _resolveTimetableEntity(this); @override String toString() { @@ -504,3 +503,102 @@ class SitTimetableLessonPart { @override String toString() => "$course at $index"; } + +SitTimetableEntity _resolveTimetableEntity(SitTimetable timetable) { + final weeks = List.generate(20, (index) => SitTimetableWeek.$7days(index)); + + for (final course in timetable.courses.values) { + final timeslots = course.timeslots; + for (final weekIndex in course.weekIndices.getWeekIndices()) { + assert( + 0 <= weekIndex && weekIndex < maxWeekLength, + "Week index is more out of range [0,$maxWeekLength) but $weekIndex.", + ); + if (0 <= weekIndex && weekIndex < maxWeekLength) { + final week = weeks[weekIndex]; + final day = week.days[course.dayIndex]; + final thatDay = reflectWeekDayIndexToDate( + weekIndex: week.index, + weekday: Weekday.fromIndex(day.index), + startDate: timetable.startDate, + ); + final fullClassTime = course.calcBeginEndTimePoint(); + final lesson = SitTimetableLesson( + course: course, + startIndex: timeslots.start, + endIndex: timeslots.end, + startTime: thatDay.addTimePoint(fullClassTime.begin), + endTime: thatDay.addTimePoint(fullClassTime.end), + ); + for (int slot = timeslots.start; slot <= timeslots.end; slot++) { + final classTime = course.calcBeginEndTimePointOfLesson(slot); + day.add( + at: slot, + lesson: SitTimetableLessonPart( + type: lesson, + index: slot, + startTime: thatDay.addTimePoint(classTime.begin), + endTime: thatDay.addTimePoint(classTime.end), + ), + ); + } + } + } + } + return SitTimetableEntity( + type: timetable, + weeks: weeks, + ); +} + +sealed class TimetableIssue {} + +class TimetableEmptyIssue implements TimetableIssue { + const TimetableEmptyIssue(); +} + +class TimetableCourseOverlapIssue implements TimetableIssue { + final List courseKeys; + final int weekIndex; + final Weekday weekday; + final ({int start, int end}) timeslots; + + const TimetableCourseOverlapIssue({ + required this.courseKeys, + required this.weekIndex, + required this.weekday, + required this.timeslots, + }); +} + +/// Two or more lessons in the same course overlap. +class TimetableDuplicateCourseOverlapIssue implements TimetableIssue { + const TimetableDuplicateCourseOverlapIssue(); +} + +extension SitTimetableX on SitTimetable { + List inspect() { + final issues = []; + // check if + if (courses.isEmpty) { + issues.add(const TimetableEmptyIssue()); + } + final entity = resolve(); + for (final week in entity.weeks) { + for (final day in week.days) { + for (var timeslot = 0; timeslot < day.timeslot2LessonSlot.length; timeslot++) { + final lessonSlot = day.timeslot2LessonSlot[timeslot]; + if (lessonSlot.lessons.length >= 2) { + issues.add(TimetableCourseOverlapIssue( + courseKeys: lessonSlot.lessons.map((l) => l.course.courseCode).toList(), + weekIndex: week.index, + weekday: Weekday.values[day.index], + timeslots: (start:timeslot,end:timeslot), + )); + } + } + } + } + return issues; + } +} diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart index d9457230d..8f80d4f2e 100644 --- a/lib/timetable/utils.dart +++ b/lib/timetable/utils.dart @@ -12,12 +12,10 @@ import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/entity/campus.dart'; import 'package:sit/files.dart'; import 'package:sit/l10n/extension.dart'; -import 'package:sit/l10n/time.dart'; import 'package:sit/school/entity/school.dart'; import 'package:sanitize_filename/sanitize_filename.dart'; import 'package:share_plus/share_plus.dart'; import 'package:sit/school/utils.dart'; -import 'package:sit/school/entity/timetable.dart'; import 'package:sit/utils/ical.dart'; import 'package:sit/utils/permission.dart'; import 'package:sit/utils/strings.dart'; @@ -138,53 +136,6 @@ SitTimetable parseUndergraduateTimetableFromCourseRaw(List SitTimetableWeek.$7days(index)); - - for (final course in timetable.courses.values) { - final timeslots = course.timeslots; - for (final weekIndex in course.weekIndices.getWeekIndices()) { - assert( - 0 <= weekIndex && weekIndex < maxWeekLength, - "Week index is more out of range [0,$maxWeekLength) but $weekIndex.", - ); - if (0 <= weekIndex && weekIndex < maxWeekLength) { - final week = weeks[weekIndex]; - final day = week.days[course.dayIndex]; - final thatDay = reflectWeekDayIndexToDate( - weekIndex: week.index, - weekday: Weekday.fromIndex(day.index), - startDate: timetable.startDate, - ); - final fullClassTime = course.calcBeginEndTimePoint(); - final lesson = SitTimetableLesson( - course: course, - startIndex: timeslots.start, - endIndex: timeslots.end, - startTime: thatDay.addTimePoint(fullClassTime.begin), - endTime: thatDay.addTimePoint(fullClassTime.end), - ); - for (int slot = timeslots.start; slot <= timeslots.end; slot++) { - final classTime = course.calcBeginEndTimePointOfLesson(slot); - day.add( - at: slot, - lesson: SitTimetableLessonPart( - type: lesson, - index: slot, - startTime: thatDay.addTimePoint(classTime.begin), - endTime: thatDay.addTimePoint(classTime.end), - ), - ); - } - } - } - } - return SitTimetableEntity( - type: timetable, - weeks: weeks, - ); -} - Duration calcuSwitchAnimationDuration(num distance) { final time = sqrt(max(1, distance) * 100000); return Duration(milliseconds: time.toInt()); diff --git a/lib/timetable/widgets/timetable/weekly.dart b/lib/timetable/widgets/timetable/weekly.dart index 8be1b0f3a..96273aa1e 100644 --- a/lib/timetable/widgets/timetable/weekly.dart +++ b/lib/timetable/widgets/timetable/weekly.dart @@ -295,7 +295,7 @@ class TimetableOneWeek extends StatelessWidget { for (int timeslot = 0; timeslot < day.timeslot2LessonSlot.length; timeslot++) { final lessonSlot = day.timeslot2LessonSlot[timeslot]; - /// TODO: Multi-layer lessonSlot + /// TODO: Multi-layer lesson slot final lesson = lessonSlot.lessonAt(0); if (lesson == null) { cells.add(DashLined( From 2f56e701e741dd82c470d49a793c5b362668ace3 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sat, 6 Apr 2024 00:59:52 +0800 Subject: [PATCH 031/458] bump some packages to the latest --- pubspec.lock | 12 ++++++------ pubspec.yaml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 0a1ea12d4..f54241098 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -637,10 +637,10 @@ packages: dependency: "direct main" description: name: flame_forge2d - sha256: cca454dbcd175c906958877c08782a8aaa77da40e718539813bfba114c75411d + sha256: "78d31cde83d30c8d9a4028296ac881334af1676ec556e4170bb81eb2829cdf27" url: "https://pub.dev" source: hosted - version: "0.17.1" + version: "0.18.0" flex_color_picker: dependency: "direct main" description: @@ -857,10 +857,10 @@ packages: dependency: transitive description: name: forge2d - sha256: a16bee7a5bdf3538ef4a51ae9b6befb39872d3518b653fc7b8756ef49f7fcf96 + sha256: "40915333b688ddaaa616d9c8ab9ff205faea0adf83dddc1a6e617694ffa9e16e" url: "https://pub.dev" source: hosted - version: "0.12.2" + version: "0.13.0" format: dependency: "direct main" description: @@ -945,10 +945,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + sha256: "36524bfb3f0b4ec952c3202466fdd69ad1f7ac1dd9b0a7564177707e45bfaeb9" url: "https://pub.dev" source: hosted - version: "7.6.7" + version: "7.6.8" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7e561b5c6..d368a4a18 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: json_annotation: ^4.8.1 copy_with_extension: ^5.0.4 freezed_annotation: ^2.4.1 - get_it: ^7.6.7 + get_it: ^7.6.8 flutter_image_compress: ^2.2.0 # Supporting scrolling screenshot on Android, like MIUI @@ -152,7 +152,7 @@ dependencies: # game flame: ^1.17.0 - flame_forge2d: ^0.17.1 + flame_forge2d: ^0.18.0 dependency_overrides: intl: ^0.19.0 From 2a42ee3ed77ab8c4e1d61ee71142dd21345de558 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sat, 6 Apr 2024 01:47:26 +0800 Subject: [PATCH 032/458] bump some packages to the latest --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f54241098..68c3dad65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -873,10 +873,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: d25416cb7144576fa565d2c4f346f5db0d3fb5dc1dd162d2814408c32946456d url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" freezed_annotation: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d368a4a18..0818a2454 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -163,7 +163,7 @@ dev_dependencies: build_runner: ^2.4.9 json_serializable: ^6.7.1 hive_generator: ^2.0.0 - freezed: ^2.4.7 + freezed: ^2.4.8 copy_with_extension_gen: ^5.0.4 test: ^1.24.9 From 1a2f63b0ba2200c40481e80e8a24fd6643d4e093 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sat, 6 Apr 2024 02:29:58 +0800 Subject: [PATCH 033/458] [timetable] last update in SitTimetable --- lib/timetable/entity/timetable.dart | 9 ++++++++- lib/timetable/service/school.demo.dart | 1 + lib/timetable/utils.dart | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/timetable/entity/timetable.dart b/lib/timetable/entity/timetable.dart index 457da0fef..59cf084a9 100644 --- a/lib/timetable/entity/timetable.dart +++ b/lib/timetable/entity/timetable.dart @@ -12,6 +12,8 @@ import '../utils.dart'; part 'timetable.g.dart'; +DateTime _kLastUpdate() => DateTime.now(); + @JsonSerializable() @CopyWith(skipFields: true) class SitTimetable { @@ -32,6 +34,9 @@ class SitTimetable { @JsonKey() final Map courses; + @JsonKey(defaultValue: _kLastUpdate) + final DateTime lastUpdate; + @JsonKey() final int version; @@ -42,6 +47,7 @@ class SitTimetable { required this.startDate, required this.schoolYear, required this.semester, + required this.lastUpdate, this.signature = "", this.version = 1, }); @@ -55,6 +61,7 @@ class SitTimetable { "startDate": startDate, "schoolYear": schoolYear, "semester": semester, + "lastUpdate": lastUpdate, "signature": signature, }.toString(); } @@ -593,7 +600,7 @@ extension SitTimetableX on SitTimetable { courseKeys: lessonSlot.lessons.map((l) => l.course.courseCode).toList(), weekIndex: week.index, weekday: Weekday.values[day.index], - timeslots: (start:timeslot,end:timeslot), + timeslots: (start: timeslot, end: timeslot), )); } } diff --git a/lib/timetable/service/school.demo.dart b/lib/timetable/service/school.demo.dart index 2a3c3baac..b2359c340 100644 --- a/lib/timetable/service/school.demo.dart +++ b/lib/timetable/service/school.demo.dart @@ -127,6 +127,7 @@ class DemoTimetableService implements TimetableService { startDate: DateTime.now(), schoolYear: info.exactYear, semester: info.semester, + lastUpdate: DateTime.now(), ); } diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart index 8f80d4f2e..f18d48a97 100644 --- a/lib/timetable/utils.dart +++ b/lib/timetable/utils.dart @@ -132,6 +132,7 @@ SitTimetable parseUndergraduateTimetableFromCourseRaw(List Date: Sat, 6 Apr 2024 23:23:08 +0800 Subject: [PATCH 034/458] [timetable] code generation --- lib/school/class2nd/service/points.dart | 2 +- lib/timetable/entity/timetable.g.dart | 8 ++++++++ pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/school/class2nd/service/points.dart b/lib/school/class2nd/service/points.dart index ee569316d..5103c927d 100644 --- a/lib/school/class2nd/service/points.dart +++ b/lib/school/class2nd/service/points.dart @@ -62,7 +62,7 @@ class Class2ndPointsService { }) _parseAllStatus(BeautifulSoup html) { // 学分=1.5(主题报告)+2.0(社会实践)+1.5(创新创业创意)+1.0(校园安全文明)+0.0(公益志愿)+2.0(校园文化) final found = html.find('#span_score'); - if(found == null){ + if (found == null) { debugPrint(found.toString()); } final String scoreText = found!.text; diff --git a/lib/timetable/entity/timetable.g.dart b/lib/timetable/entity/timetable.g.dart index 344f0a603..9e1876d74 100644 --- a/lib/timetable/entity/timetable.g.dart +++ b/lib/timetable/entity/timetable.g.dart @@ -20,6 +20,7 @@ abstract class _$SitTimetableCWProxy { DateTime? startDate, int? schoolYear, Semester? semester, + DateTime? lastUpdate, String? signature, int? version, }); @@ -46,6 +47,7 @@ class _$SitTimetableCWProxyImpl implements _$SitTimetableCWProxy { Object? startDate = const $CopyWithPlaceholder(), Object? schoolYear = const $CopyWithPlaceholder(), Object? semester = const $CopyWithPlaceholder(), + Object? lastUpdate = const $CopyWithPlaceholder(), Object? signature = const $CopyWithPlaceholder(), Object? version = const $CopyWithPlaceholder(), }) { @@ -74,6 +76,10 @@ class _$SitTimetableCWProxyImpl implements _$SitTimetableCWProxy { ? _value.semester // ignore: cast_nullable_to_non_nullable : semester as Semester, + lastUpdate: lastUpdate == const $CopyWithPlaceholder() || lastUpdate == null + ? _value.lastUpdate + // ignore: cast_nullable_to_non_nullable + : lastUpdate as DateTime, signature: signature == const $CopyWithPlaceholder() || signature == null ? _value.signature // ignore: cast_nullable_to_non_nullable @@ -259,6 +265,7 @@ SitTimetable _$SitTimetableFromJson(Map json) => SitTimetable( startDate: DateTime.parse(json['startDate'] as String), schoolYear: json['schoolYear'] as int, semester: $enumDecode(_$SemesterEnumMap, json['semester']), + lastUpdate: json['lastUpdate'] == null ? _kLastUpdate() : DateTime.parse(json['lastUpdate'] as String), signature: json['signature'] as String? ?? "", version: json['version'] as int? ?? 1, ); @@ -271,6 +278,7 @@ Map _$SitTimetableToJson(SitTimetable instance) => Date: Sun, 7 Apr 2024 12:25:04 +0800 Subject: [PATCH 035/458] [expense] [statistics] l10n of chart --- lib/l10n/extension.dart | 1 + lib/life/expense_records/utils.dart | 22 +++++++++---------- .../expense_records/widget/chart/bar.dart | 9 +++++--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/l10n/extension.dart b/lib/l10n/extension.dart index 988386ba1..1afc0296c 100644 --- a/lib/l10n/extension.dart +++ b/lib/l10n/extension.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:sit/l10n/time.dart'; import 'lang.dart'; diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 9759c4831..618dba706 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; +import 'package:sit/l10n/extension.dart'; import 'package:sit/life/expense_records/entity/local.dart'; import 'package:sit/school/utils.dart'; @@ -141,16 +142,15 @@ Map records, double tota }); } -final _monthFormat = DateFormat.MMMM(); -final _monthDayFormat = DateFormat.Md(); -final _yearMonthFormat = DateFormat.yMMMM(); -final _yearFormat = DateFormat.y(); - String resolveTime4Display({ required BuildContext context, required StatisticsMode mode, required DateTime date, }) { + final monthFormat = DateFormat.MMMM(); + final monthDayFormat = DateFormat.Md(); + final yearMonthFormat = DateFormat.yMMMM(); + final yearFormat = DateFormat.y(); final now = DateTime.now(); switch (mode) { case StatisticsMode.week: @@ -162,11 +162,11 @@ String resolveTime4Display({ } else if (dateWeek == nowWeek - 1) { return "Last week"; } else { - return "? week ${_yearFormat.format(date)}"; - // return "${_monthDayFormat.format(date)}"; + return "? week ${yearFormat.format(date)}"; + // return "${monthDayFormat.format(date)}"; } } else { - return "? week ${_yearFormat.format(date)}"; + return "? week ${yearFormat.format(date)}"; } case StatisticsMode.month: if (date.year == now.year) { @@ -175,10 +175,10 @@ String resolveTime4Display({ } else if (date.month == now.month - 1) { return "Last month"; } else { - return _monthFormat.format(date); + return monthFormat.format(date); } } else { - return _yearMonthFormat.format(date); + return yearMonthFormat.format(date); } case StatisticsMode.year: if (date.year == now.year) { @@ -186,7 +186,7 @@ String resolveTime4Display({ } else if (date.year == now.year - 1) { return "Last year"; } else { - return _yearFormat.format(date); + return yearFormat.format(date); } } } diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 96ae694d6..fd5e55472 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -6,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/l10n/time.dart'; +import 'package:sit/route.dart'; import 'package:sit/utils/date.dart'; import '../../entity/local.dart'; @@ -47,8 +50,6 @@ class _ExpenseLineChartState extends State { } } -final _monthFormat = DateFormat.MMM(); - class StatisticsDelegate { final List data; final StatisticsMode mode; @@ -141,6 +142,7 @@ class StatisticsDelegate { required DateTime start, required List records, }) { + final _monthFormat = DateFormat.MMM($Key.currentContext!.locale.toString()); final now = DateTime.now(); final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); for (final record in records) { @@ -153,10 +155,11 @@ class StatisticsDelegate { side: _buildSideTitle, bottom: (value, mate) { final index = value.toInt(); + final text = _monthFormat.format(DateTime(0, index + 1)); return SideTitleWidget( axisSide: mate.axisSide, child: Text( - _monthFormat.format(DateTime(0, index + 1)).substring(0, 3), + text.substring(0, min(3, text.length)), ), ); }, From c70d1bb8d183793b2b378674eb12e9aa6a8fc957 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 7 Apr 2024 20:13:17 +0800 Subject: [PATCH 036/458] [expense] [statistics] renaming --- .../expense_records/entity/statistics.dart | 12 +++---- lib/life/expense_records/page/statistics.dart | 4 ++- .../expense_records/widget/chart/bar.dart | 32 ++++++++++--------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index cce98f622..4071e61e8 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -7,9 +7,9 @@ import 'local.dart'; typedef StartTime2Records = List<({DateTime start, List records})>; enum StatisticsMode { - week(_weekly), - month(_monthly), - year(_yearly); + week(_week), + month(_month), + year(_year); /// Resort the records, separate them by start time, and sort them in DateTime ascending order. final StartTime2Records Function(List records) resort; @@ -19,7 +19,7 @@ enum StatisticsMode { String l10nName() => "expenseRecords.statsMode.$name".tr(); } -StartTime2Records _weekly(List records) { +StartTime2Records _week(List records) { final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); final startTime2Records = ym2records.entries .map((entry) => (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) @@ -28,7 +28,7 @@ StartTime2Records _weekly(List records) { return startTime2Records; } -StartTime2Records _monthly(List records) { +StartTime2Records _month(List records) { final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.month)); final startTime2Records = ym2records.entries.map((entry) => (start: DateTime(entry.key.$1, entry.key.$2), records: entry.value)).toList(); @@ -36,7 +36,7 @@ StartTime2Records _monthly(List records) { return startTime2Records; } -StartTime2Records _yearly(List records) { +StartTime2Records _year(List records) { final ym2records = records.groupListsBy((r) => r.timestamp.year); final startTime2Records = ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index af4238cee..28e025867 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -102,14 +102,16 @@ class _ExpenseStatisticsPageState extends ConsumerState { body: [ ListView( controller: controller, + physics: const AlwaysScrollableScrollPhysics(), children: [ buildModeSelector(mode).padSymmetric(h: 16, v: 4), - ExpenseLineChart( + ExpenseBarChart( start: current.start, records: current.records, mode: mode, ), ExpensePieChart(records: separated), + const Divider(), ...current.records.map((record) { return TransactionTile(record); }), diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 96ae694d6..8298fc657 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -12,12 +12,12 @@ import '../../entity/local.dart'; import '../../entity/statistics.dart'; import "../../i18n.dart"; -class ExpenseLineChart extends StatefulWidget { +class ExpenseBarChart extends StatefulWidget { final DateTime start; final StatisticsMode mode; final List records; - const ExpenseLineChart({ + const ExpenseBarChart({ super.key, required this.start, required this.records, @@ -25,10 +25,10 @@ class ExpenseLineChart extends StatefulWidget { }); @override - State createState() => _ExpenseLineChartState(); + State createState() => _ExpenseBarChartState(); } -class _ExpenseLineChartState extends State { +class _ExpenseBarChartState extends State { @override Widget build(BuildContext context) { final delegate = StatisticsDelegate.byMode( @@ -52,8 +52,8 @@ final _monthFormat = DateFormat.MMM(); class StatisticsDelegate { final List data; final StatisticsMode mode; - final GetTitleWidgetFunction side; - final GetTitleWidgetFunction bottom; + final Widget Function(BuildContext context, double value, TitleMeta meta) side; + final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; const StatisticsDelegate({ required this.mode, @@ -94,8 +94,8 @@ class StatisticsDelegate { return StatisticsDelegate( mode: StatisticsMode.week, data: weekAmount, - side: _buildSideTitle, - bottom: (value, mate) { + side: (ctx,v,meta)=> _buildSideTitle(v,meta), + bottom: (ctx,value, mate) { final index = value.toInt(); return SideTitleWidget( axisSide: mate.axisSide, @@ -124,8 +124,8 @@ class StatisticsDelegate { return StatisticsDelegate( mode: StatisticsMode.week, data: dayAmount, - side: _buildSideTitle, - bottom: (value, mate) { + side: (ctx,v,meta)=> _buildSideTitle(v,meta), + bottom: (ctx,value, mate) { final index = value.toInt(); return SideTitleWidget( axisSide: mate.axisSide, @@ -150,8 +150,8 @@ class StatisticsDelegate { return StatisticsDelegate( mode: StatisticsMode.week, data: monthAmount, - side: _buildSideTitle, - bottom: (value, mate) { + side: (ctx,v,meta)=> _buildSideTitle(v,meta), + bottom: (ctx,value, mate) { final index = value.toInt(); return SideTitleWidget( axisSide: mate.axisSide, @@ -167,7 +167,9 @@ class StatisticsDelegate { String text = '¥${value.round()}'; return SideTitleWidget( axisSide: meta.axisSide, - child: Text(text), + child: Text( + text, + ), ); } } @@ -192,14 +194,14 @@ class AmountChartWidget extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 28, - getTitlesWidget: delegate.bottom, + getTitlesWidget:(v,meta)=> delegate.bottom(context,v,meta), ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, - getTitlesWidget: delegate.side, + getTitlesWidget:(v,meta)=> delegate.side(context,v,meta), ), ), topTitles: const AxisTitles( From dd96ecf6abfd583349417aebbfc488b9a033f136 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 7 Apr 2024 20:42:44 +0800 Subject: [PATCH 037/458] [expense] [statistics] hide some bottom titles --- lib/life/expense_records/widget/chart/bar.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 6b78ff10a..1efb491d1 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -122,12 +122,16 @@ class StatisticsDelegate { // add data on the same day. dayAmount[record.timestamp.day - 1] += record.deltaAmount; } + final sep = dayAmount.length ~/ 5; return StatisticsDelegate( mode: StatisticsMode.week, data: dayAmount, side: (ctx,v,meta)=> _buildSideTitle(v,meta), bottom: (ctx,value, mate) { final index = value.toInt(); + if(!(index == 0 || index == dayAmount.length - 1) && index % sep != 0){ + return const SizedBox(); + } return SideTitleWidget( axisSide: mate.axisSide, child: Text( From fd6c07900ca74354b04a241e081ea4c5fcc5a64a Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 7 Apr 2024 22:17:11 +0800 Subject: [PATCH 038/458] [expense] [statistics] separate by type in bar chart --- assets/l10n/en.yaml | 6 +- lib/life/expense_records/page/statistics.dart | 4 +- lib/life/expense_records/utils.dart | 14 ++-- .../expense_records/widget/chart/bar.dart | 84 +++++++++++-------- .../expense_records/widget/transaction.dart | 2 + 5 files changed, 63 insertions(+), 47 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index da011f712..6b2ddc60d 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -443,11 +443,11 @@ expenseRecords: consume: Consume coffee: Café food: Canteen - store: Grocery Shopping - water: Hot Water + store: Grocery + water: Water library: Library shower: Shower - topUp: Top Up + topUp: Top up subsidy: Subsidy other: Stuff homepage: diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 28e025867..4a064ba81 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -94,7 +94,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { }); }); final current = startTime2Records.indexAt(index); - final separated = separateTransactionByType(current.records); + final (total: _, :parts) = separateTransactionByType(current.records); return Scaffold( appBar: AppBar( title: i18n.stats.title.text(), @@ -110,7 +110,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { records: current.records, mode: mode, ), - ExpensePieChart(records: separated), + ExpensePieChart(records: parts), const Divider(), ...current.records.map((record) { return TransactionTile(record); diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 618dba706..0d833d99c 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -130,16 +130,20 @@ double accumulateTransactionAmount(List transactions) { return total; } -Map records, double total})> separateTransactionByType( +({double total, Map records, double total})> parts}) + separateTransactionByType( List records, ) { final type2transactions = records.groupListsBy((record) => record.type); final type2total = type2transactions.map((type, records) => MapEntry(type, accumulateTransactionAmount(records))); final total = type2total.values.sum; - return type2transactions.map((type, records) { - final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); - return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); - }); + return ( + total: total, + parts: type2transactions.map((type, records) { + final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); + return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); + }) + ); } String resolveTime4Display({ diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 1efb491d1..31da384e8 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:collection/collection.dart'; -import 'package:dynamic_color/dynamic_color.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; @@ -14,6 +13,7 @@ import 'package:sit/utils/date.dart'; import '../../entity/local.dart'; import '../../entity/statistics.dart'; import "../../i18n.dart"; +import '../../utils.dart'; class ExpenseBarChart extends StatefulWidget { final DateTime start; @@ -51,7 +51,7 @@ class _ExpenseBarChartState extends State { } class StatisticsDelegate { - final List data; + final List> data; final StatisticsMode mode; final Widget Function(BuildContext context, double value, TitleMeta meta) side; final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; @@ -83,20 +83,20 @@ class StatisticsDelegate { required List records, }) { final now = DateTime.now(); - final List weekAmount = List.filled( + final data = List.generate( start.year == now.year && start.week == now.week ? now.weekday : 7, - 0.00, + (i) => [], ); for (final record in records) { // add data at the same weekday. // sunday goes first - weekAmount[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday] += record.deltaAmount; + data[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday].add(record); } return StatisticsDelegate( mode: StatisticsMode.week, - data: weekAmount, - side: (ctx,v,meta)=> _buildSideTitle(v,meta), - bottom: (ctx,value, mate) { + data: data, + side: (ctx, v, meta) => _buildSideTitle(v, meta), + bottom: (ctx, value, mate) { final index = value.toInt(); return SideTitleWidget( axisSide: mate.axisSide, @@ -113,27 +113,26 @@ class StatisticsDelegate { required List records, }) { final now = DateTime.now(); - final List dayAmount = List.filled( - start.year == now.year && start.month == now.month - ? now.day - : daysInMonth(year: start.year, month: start.month), - 0.00); + final data = List.generate( + start.year == now.year && start.month == now.month ? now.day : daysInMonth(year: start.year, month: start.month), + (i) => [], + ); for (final record in records) { // add data on the same day. - dayAmount[record.timestamp.day - 1] += record.deltaAmount; + data[record.timestamp.day - 1].add(record); } - final sep = dayAmount.length ~/ 5; + final sep = data.length ~/ 5; return StatisticsDelegate( mode: StatisticsMode.week, - data: dayAmount, - side: (ctx,v,meta)=> _buildSideTitle(v,meta), - bottom: (ctx,value, mate) { + data: data, + side: (ctx, v, meta) => _buildSideTitle(v, meta), + bottom: (ctx, value, meta) { final index = value.toInt(); - if(!(index == 0 || index == dayAmount.length - 1) && index % sep != 0){ + if (!(index == 0 || index == data.length - 1) && index % sep != 0) { return const SizedBox(); } return SideTitleWidget( - axisSide: mate.axisSide, + axisSide: meta.axisSide, child: Text( "${index + 1}", ), @@ -148,16 +147,16 @@ class StatisticsDelegate { }) { final _monthFormat = DateFormat.MMM($Key.currentContext!.locale.toString()); final now = DateTime.now(); - final List monthAmount = List.filled(start.year == now.year ? now.month : 12, 0.00); + final data = List.generate(start.year == now.year ? now.month : 12, (i) => []); for (final record in records) { // add data in the same month. - monthAmount[record.timestamp.month - 1] += record.deltaAmount; + data[record.timestamp.month - 1].add(record); } return StatisticsDelegate( mode: StatisticsMode.week, - data: monthAmount, - side: (ctx,v,meta)=> _buildSideTitle(v,meta), - bottom: (ctx,value, mate) { + data: data, + side: (ctx, v, meta) => _buildSideTitle(v, meta), + bottom: (ctx, value, mate) { final index = value.toInt(); final text = _monthFormat.format(DateTime(0, index + 1)); return SideTitleWidget( @@ -201,14 +200,14 @@ class AmountChartWidget extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 28, - getTitlesWidget:(v,meta)=> delegate.bottom(context,v,meta), + getTitlesWidget: (v, meta) => delegate.bottom(context, v, meta), ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, - getTitlesWidget:(v,meta)=> delegate.side(context,v,meta), + getTitlesWidget: (v, meta) => delegate.side(context, v, meta), ), ), topTitles: const AxisTitles( @@ -231,16 +230,27 @@ class AmountChartWidget extends StatelessWidget { show: false, ), groupsSpace: 40, - barGroups: delegate.data - .mapIndexed((i, v) => BarChartGroupData( - x: i, - barRods: [ - BarChartRodData( - toY: v, - ), - ], - )) - .toList(), + barGroups: delegate.data.mapIndexed((i, records) { + final (:total, :parts) = separateTransactionByType(records); + var c = 0.0; + return BarChartGroupData( + x: i, + barRods: [ + BarChartRodData( + toY: total, + rodStackItems: parts.entries.map((e) { + final res = BarChartRodStackItem( + c, + c + e.value.total, + e.key.color, + ); + c += e.value.total; + return res; + }).toList(), + ), + ], + ); + }).toList(), ), ); } diff --git a/lib/life/expense_records/widget/transaction.dart b/lib/life/expense_records/widget/transaction.dart index 16597fddc..ec4bf593a 100644 --- a/lib/life/expense_records/widget/transaction.dart +++ b/lib/life/expense_records/widget/transaction.dart @@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:sit/l10n/extension.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/settings/dev.dart'; import '../entity/local.dart'; import "../i18n.dart"; @@ -18,6 +19,7 @@ class TransactionTile extends StatelessWidget { subtitle: [ context.formatYmdhmsNum(transaction.timestamp).text(), if (title != transaction.note) transaction.note.text(), + if (Dev.on) "${transaction.balanceBefore} => ${transaction.balanceAfter}".text(), ].column(caa: CrossAxisAlignment.start), leading: transaction.type.icon.make(color: transaction.type.color, size: 32), trailing: transaction.toReadableString().text( From a37e5572413e7e685e20704c82677827f8e5964b Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 10:29:39 +0800 Subject: [PATCH 039/458] bump some packages to the latest --- pubspec.lock | 12 ++++++------ pubspec.yaml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 94c5c5239..e270b25a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -873,10 +873,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "24f77b50776d4285cc4b3a1665bb79852714c09b878363efbe64788c179c4284" + sha256: "91bce569d4805ea5bad6619a3e8690df8ad062a235165af4c0c5d928dda15eaf" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" freezed_annotation: dependency: "direct main" description: @@ -961,10 +961,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "5ed2687bc961f33a752017ccaa7edead3e5601b28b6376a5901bf24728556b85" + sha256: f6ba8eed5fa831e461122de577d4a26674a1d836e2956abe6c0f6c4d952e6673 url: "https://pub.dev" source: hosted - version: "13.2.2" + version: "13.2.3" graphs: dependency: transitive description: @@ -2110,10 +2110,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bc4c3d895..66d40ad8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: # Hash (MD5) crypto: ^3.0.3 # UUID generator - uuid: ^4.3.3 + uuid: ^4.4.0 # HTML parser beautiful_soup_dart: ^0.3.0 @@ -102,7 +102,7 @@ dependencies: add_2_calendar: ^3.0.1 # UI - go_router: ^13.2.2 + go_router: ^13.2.3 fl_chart: ^0.67.0 flutter_svg: ^2.0.10+1 flutter_svg_provider: ^1.0.7 @@ -163,7 +163,7 @@ dev_dependencies: build_runner: ^2.4.9 json_serializable: ^6.7.1 hive_generator: ^2.0.0 - freezed: ^2.5.0 + freezed: ^2.5.1 copy_with_extension_gen: ^5.0.4 test: ^1.24.9 From bdd3a6e033f559a778bce87304c23195ac942a13 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 10:56:12 +0800 Subject: [PATCH 040/458] [expense] [statistics] average line --- .../expense_records/widget/chart/bar.dart | 26 +++++++++++++++++++ pubspec.lock | 24 +++++++++++++++++ pubspec.yaml | 2 ++ 3 files changed, 52 insertions(+) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 31da384e8..75e2320ef 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -9,6 +9,7 @@ import 'package:rettulf/rettulf.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/route.dart'; import 'package:sit/utils/date.dart'; +import 'package:statistics/statistics.dart'; import '../../entity/local.dart'; import '../../entity/statistics.dart'; @@ -52,6 +53,7 @@ class _ExpenseBarChartState extends State { class StatisticsDelegate { final List> data; + final double average; final StatisticsMode mode; final Widget Function(BuildContext context, double value, TitleMeta meta) side; final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; @@ -59,6 +61,7 @@ class StatisticsDelegate { const StatisticsDelegate({ required this.mode, required this.data, + required this.average, required this.side, required this.bottom, }); @@ -92,10 +95,12 @@ class StatisticsDelegate { // sunday goes first data[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday].add(record); } + final amounts = records.map((r) => r.deltaAmount).toList(); return StatisticsDelegate( mode: StatisticsMode.week, data: data, side: (ctx, v, meta) => _buildSideTitle(v, meta), + average: amounts.isEmpty ? double.infinity : amounts.mean, bottom: (ctx, value, mate) { final index = value.toInt(); return SideTitleWidget( @@ -121,10 +126,12 @@ class StatisticsDelegate { // add data on the same day. data[record.timestamp.day - 1].add(record); } + final amounts = records.map((r) => r.deltaAmount).toList(); final sep = data.length ~/ 5; return StatisticsDelegate( mode: StatisticsMode.week, data: data, + average: amounts.isEmpty ? double.infinity : amounts.mean, side: (ctx, v, meta) => _buildSideTitle(v, meta), bottom: (ctx, value, meta) { final index = value.toInt(); @@ -152,9 +159,11 @@ class StatisticsDelegate { // add data in the same month. data[record.timestamp.month - 1].add(record); } + final amounts = records.map((r) => r.deltaAmount).toList(); return StatisticsDelegate( mode: StatisticsMode.week, data: data, + average: amounts.isEmpty ? double.infinity : amounts.mean, side: (ctx, v, meta) => _buildSideTitle(v, meta), bottom: (ctx, value, mate) { final index = value.toInt(); @@ -217,6 +226,23 @@ class AmountChartWidget extends StatelessWidget { sideTitles: SideTitles(showTitles: false), ), ), + extraLinesData: ExtraLinesData( + horizontalLines: [ + if (delegate.average.isFinite) + HorizontalLine( + label: HorizontalLineLabel( + show: true, + alignment: Alignment.bottomRight, + style: context.textTheme.labelSmall, + labelResolver: (line) => "¥${line.y.toStringAsFixed(2)}", + ), + y: delegate.average, + strokeWidth: 3, + color: context.colorScheme.tertiary.withOpacity(0.5), + dashArray: [5, 5], + ), + ], + ), gridData: FlGridData( show: true, checkToShowHorizontalLine: (value) => value % 5 == 0, diff --git a/pubspec.lock b/pubspec.lock index e270b25a5..d860a0fd7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + base_codecs: + dependency: transitive + description: + name: base_codecs + sha256: "41701a12ede9912663decd708279924ece5018566daa7d1f484d5f4f10894f91" + url: "https://pub.dev" + source: hosted + version: "1.0.1" basic_utils: dependency: transitive description: @@ -393,6 +401,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + data_serializer: + dependency: transitive + description: + name: data_serializer + sha256: "44f0e3f2b82f54bce125f2f8ed7900e5d5700603c5fa033dcc1d89b0714da3a6" + url: "https://pub.dev" + source: hosted + version: "1.1.0" dbus: dependency: transitive description: @@ -1898,6 +1914,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + statistics: + dependency: "direct main" + description: + name: statistics + sha256: "0640da6e7a35d2e5f63249130ce1ad9551dce9ed99de4c0c1c3ddf450c157f74" + url: "https://pub.dev" + source: hosted + version: "1.1.0" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 66d40ad8a..20ac3d277 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: crypto: ^3.0.3 # UUID generator uuid: ^4.4.0 + # Statistics + statistics: ^1.1.0 # HTML parser beautiful_soup_dart: ^0.3.0 From d00282c7f96a0528bc1cfe4a412c5eda8835a542 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 11:37:18 +0800 Subject: [PATCH 041/458] [expense] [statistics] display total --- lib/life/expense_records/page/statistics.dart | 4 +- .../expense_records/widget/chart/bar.dart | 56 ++++++++++++++----- .../expense_records/widget/chart/pie.dart | 33 +++++------ 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 4a064ba81..c1b4a2095 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -94,7 +94,6 @@ class _ExpenseStatisticsPageState extends ConsumerState { }); }); final current = startTime2Records.indexAt(index); - final (total: _, :parts) = separateTransactionByType(current.records); return Scaffold( appBar: AppBar( title: i18n.stats.title.text(), @@ -110,7 +109,8 @@ class _ExpenseStatisticsPageState extends ConsumerState { records: current.records, mode: mode, ), - ExpensePieChart(records: parts), + const Divider(), + ExpensePieChart(records: current.records), const Divider(), ...current.records.map((record) { return TransactionTile(record); diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 75e2320ef..9c90572c8 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -1,10 +1,9 @@ import 'dart:math'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' hide IterableDoubleExtension; import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/route.dart'; @@ -35,25 +34,30 @@ class ExpenseBarChart extends StatefulWidget { class _ExpenseBarChartState extends State { @override Widget build(BuildContext context) { + assert(widget.records.every((type) => type.isConsume)); final delegate = StatisticsDelegate.byMode( widget.mode, start: widget.start, records: widget.records, ); - return OutlinedCard( - child: AspectRatio( + return [ + ExpenseBarChartHeader( + total: delegate.total, + ).padFromLTRB(16,8,0,8), + AspectRatio( aspectRatio: 1.5, child: AmountChartWidget( delegate: delegate, ).padSymmetric(v: 12, h: 8), ), - ); + ].column(caa: CrossAxisAlignment.start); } } class StatisticsDelegate { final List> data; final double average; + final double total; final StatisticsMode mode; final Widget Function(BuildContext context, double value, TitleMeta meta) side; final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; @@ -62,6 +66,7 @@ class StatisticsDelegate { required this.mode, required this.data, required this.average, + required this.total, required this.side, required this.bottom, }); @@ -99,13 +104,15 @@ class StatisticsDelegate { return StatisticsDelegate( mode: StatisticsMode.week, data: data, - side: (ctx, v, meta) => _buildSideTitle(v, meta), + side: _buildSideTitle, average: amounts.isEmpty ? double.infinity : amounts.mean, + total: amounts.sum, bottom: (ctx, value, mate) { final index = value.toInt(); return SideTitleWidget( axisSide: mate.axisSide, child: Text( + style: ctx.textTheme.labelMedium, Weekday.calendarOrder[index].l10nShort(), ), ); @@ -132,7 +139,8 @@ class StatisticsDelegate { mode: StatisticsMode.week, data: data, average: amounts.isEmpty ? double.infinity : amounts.mean, - side: (ctx, v, meta) => _buildSideTitle(v, meta), + total: amounts.sum, + side: _buildSideTitle, bottom: (ctx, value, meta) { final index = value.toInt(); if (!(index == 0 || index == data.length - 1) && index % sep != 0) { @@ -141,6 +149,7 @@ class StatisticsDelegate { return SideTitleWidget( axisSide: meta.axisSide, child: Text( + style: ctx.textTheme.labelMedium, "${index + 1}", ), ); @@ -164,13 +173,15 @@ class StatisticsDelegate { mode: StatisticsMode.week, data: data, average: amounts.isEmpty ? double.infinity : amounts.mean, - side: (ctx, v, meta) => _buildSideTitle(v, meta), + total: amounts.sum, + side: _buildSideTitle, bottom: (ctx, value, mate) { final index = value.toInt(); final text = _monthFormat.format(DateTime(0, index + 1)); return SideTitleWidget( axisSide: mate.axisSide, child: Text( + style: ctx.textTheme.labelMedium, text.substring(0, min(3, text.length)), ), ); @@ -178,11 +189,12 @@ class StatisticsDelegate { ); } - static Widget _buildSideTitle(double value, TitleMeta meta) { + static Widget _buildSideTitle(BuildContext ctx, double value, TitleMeta meta) { String text = '¥${value.round()}'; return SideTitleWidget( axisSide: meta.axisSide, child: Text( + style: ctx.textTheme.labelMedium, text, ), ); @@ -215,7 +227,7 @@ class AmountChartWidget extends StatelessWidget { leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 40, + reservedSize: 50, getTitlesWidget: (v, meta) => delegate.side(context, v, meta), ), ), @@ -232,9 +244,9 @@ class AmountChartWidget extends StatelessWidget { HorizontalLine( label: HorizontalLineLabel( show: true, - alignment: Alignment.bottomRight, - style: context.textTheme.labelSmall, - labelResolver: (line) => "¥${line.y.toStringAsFixed(2)}", + alignment: Alignment.bottomRight, + style: context.textTheme.labelSmall, + labelResolver: (line) => "¥${line.y.toStringAsFixed(2)}", ), y: delegate.average, strokeWidth: 3, @@ -281,3 +293,21 @@ class AmountChartWidget extends StatelessWidget { ); } } + + +class ExpenseBarChartHeader extends StatelessWidget { + final double total; + + const ExpenseBarChartHeader({ + super.key, + required this.total, + }); + + @override + Widget build(BuildContext context) { + return [ + "Total".text(style: context.textTheme.titleMedium?.copyWith(color:context.theme.disabledColor)), + "¥${total.toStringAsFixed(2)}".text(style: context.textTheme.titleLarge), + ].column(caa: CrossAxisAlignment.start); + } +} diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index b946b157c..0507c3401 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -8,9 +8,10 @@ import 'package:sit/design/widgets/card.dart'; import '../../entity/local.dart'; import "../../i18n.dart"; +import '../../utils.dart'; class ExpensePieChart extends StatefulWidget { - final Map records, double total, double proportion})> records; + final List records; const ExpensePieChart({ super.key, @@ -26,19 +27,19 @@ class _ExpensePieChartState extends State { @override Widget build(BuildContext context) { - assert(widget.records.keys.every((type) => type.isConsume)); - return OutlinedCard( - child: [ - AspectRatio( - aspectRatio: 1.5, - child: buildChart(), - ), - buildLegends().padAll(8).align(at: Alignment.topLeft), - ].column(), - ); + assert(widget.records.every((type) => type.isConsume)); + final (:total, :parts) = separateTransactionByType(widget.records); + + return [ + AspectRatio( + aspectRatio: 1.5, + child: buildChart(parts), + ), + buildLegends(parts).padAll(8).align(at: Alignment.topLeft), + ].column(caa: CrossAxisAlignment.start); } - Widget buildChart() { + Widget buildChart(Map records, double total})> parts) { return PieChart( PieChartData( pieTouchData: PieTouchData( @@ -59,7 +60,7 @@ class _ExpensePieChartState extends State { ), sectionsSpace: 0, centerSpaceRadius: 60, - sections: widget.records.entries.mapIndexed((i, entry) { + sections: parts.entries.mapIndexed((i, entry) { final isTouched = i == touchedIndex; final MapEntry(key: type, value: (records: _, :total, :proportion)) = entry; final color = type.color.harmonizeWith(context.colorScheme.primary); @@ -77,8 +78,8 @@ class _ExpensePieChartState extends State { ); } - Widget buildLegends() { - return widget.records.entries + Widget buildLegends(Map records, double total})> parts) { + return parts.entries .map((record) { final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; final color = type.color.harmonizeWith(context.colorScheme.primary); @@ -89,6 +90,6 @@ class _ExpensePieChartState extends State { ); }) .toList() - .wrap(spacing: 4); + .wrap(spacing: 4,runSpacing: 4); } } From fe8b1757bbcf49d19e54942258240a9bce9ea278 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 12:41:17 +0800 Subject: [PATCH 042/458] [timetable] inClassNow --- lib/timetable/widgets/timetable/weekly.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/timetable/widgets/timetable/weekly.dart b/lib/timetable/widgets/timetable/weekly.dart index 96273aa1e..8c37f4aeb 100644 --- a/lib/timetable/widgets/timetable/weekly.dart +++ b/lib/timetable/widgets/timetable/weekly.dart @@ -147,18 +147,27 @@ class _TimetableOneWeekCachedState extends State with Au return cache; } else { final style = TimetableStyle.of(context); - final today = DateTime.now(); + final now = DateTime.now(); Widget buildCell({ required BuildContext context, required SitTimetableLessonPart lesson, required SitTimetableEntity timetable, }) { - return InteractiveCourseCell( + final inClassNow = lesson.type.startTime.isBefore(now) && lesson.type.endTime.isAfter(now); + final passed = lesson.type.endTime.isBefore(now); + Widget cell = InteractiveCourseCell( lesson: lesson, style: style, timetable: timetable, - grayOut: style.cellStyle.grayOutTakenLessons ? lesson.type.endTime.isBefore(today) : false, + grayOut: style.cellStyle.grayOutTakenLessons ? passed : false, ); + if (inClassNow) { + // cell = OutlinedCard( + // margin: const EdgeInsets.all(1), + // child: cell.padAll(1), + // ); + } + return cell; } final res = LayoutBuilder( From 2373b9952c6d7c22fd2b65c44b11932dd6261235 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 14:10:16 +0800 Subject: [PATCH 043/458] [timetable] fixed importing from files not working --- lib/timetable/utils.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart index f18d48a97..c439f93e8 100644 --- a/lib/timetable/utils.dart +++ b/lib/timetable/utils.dart @@ -142,7 +142,7 @@ Duration calcuSwitchAnimationDuration(num distance) { return Duration(milliseconds: time.toInt()); } -Future readTimetableFromFile() async { +Future readTimetableFromPickedFile() async { final result = await FilePicker.platform.pickFiles( // Cannot limit the extensions. My RedMi phone just reject all files. // type: FileType.custom, @@ -158,8 +158,8 @@ Future readTimetableFromFile() async { Future readTimetableFromFileWithPrompt(BuildContext context) async { try { - final id2timetable = await readTimetableFromFile(); - if (id2timetable == null) return null; + final timetable = await readTimetableFromPickedFile(); + return timetable; } catch (err, stackTrace) { debugPrint(err.toString()); debugPrintStack(stackTrace: stackTrace); @@ -175,7 +175,6 @@ Future readTimetableFromFileWithPrompt(BuildContext context) asyn } return null; } - return null; } Future _readTimetableFi(PlatformFile fi) async { From d4ae8b7f8050b3c0d15e0a80e85967d1a6edaa3f Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 14:35:41 +0800 Subject: [PATCH 044/458] [workflow] build web preview --- .github/workflows/build.yml | 4 ++-- .github/workflows/web.yml | 24 ++++++++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/web.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index faf8675ba..a4c91da4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: distribution: 'temurin' java-version: '17' - - name: Install and set Flutter version + - name: Install Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.flutter_version }} @@ -105,7 +105,7 @@ jobs: mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - - name: Install and set Flutter version + - name: Install Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.flutter_version }} diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml new file mode 100644 index 000000000..67d783e91 --- /dev/null +++ b/.github/workflows/web.yml @@ -0,0 +1,24 @@ +name: Web +on: workflow_dispatch + +env: + flutter_version: '3.19.5' + +jobs: + build: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.flutter_version }} + channel: stable + cache: true + + - uses: bluefireteam/flutter-gh-pages@v7 + with: + baseHref: /mimir/ diff --git a/pubspec.lock b/pubspec.lock index d860a0fd7..5d5532dc5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2372,4 +2372,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.3" + flutter: ">=3.19.5" diff --git a/pubspec.yaml b/pubspec.yaml index 20ac3d277..703766540 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ issue_tracker: https://github.com/liplum/mimir/issues documentation: https://github.com/liplum/mimir#readme publish_to: none -environment: { sdk: '>=3.3.0 <4.0.0', flutter: ^3.19.3 } +environment: { sdk: '>=3.3.0 <4.0.0', flutter: ^3.19.5 } dependencies: flutter: { sdk: flutter } From 75e911c06304faab5b4ae0511d80f226c21508de Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 14:39:27 +0800 Subject: [PATCH 045/458] download buttons in README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index aa2dfa4ae..eb8e6838a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Icon +[![Buttons Download]][Download] + ### A multiplatform app for SIT students. *Android, iOS, Windows, macOS, and Linux* @@ -29,3 +31,11 @@ Welcome to contribute SIT Life, please read the [contribution guide](specificati The source codes and configurations are open source under [GPL v3](LICENSE). + + + +[Download]: https://github.com/liplum-dev/mimir/releases/latest + + + +[Buttons Download]: https://img.shields.io/github/downloads/liplum-dev/mimir/total?color=023a46&label=Download&logo=docusign&logoColor=white&style=for-the-badge&labelColor=034e5e From 5892d10236041cf17d64ac26d628c7bcb8da645d Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 14:42:56 +0800 Subject: [PATCH 046/458] [workflow] permissions: write-all --- .github/workflows/web.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 67d783e91..de119ce24 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -1,5 +1,6 @@ name: Web on: workflow_dispatch +permissions: write-all env: flutter_version: '3.19.5' From 71fd425b87abfc1b902b1bc58aa14bc8abdef5fc Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 14:53:28 +0800 Subject: [PATCH 047/458] online preview buttons in README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index eb8e6838a..5f69bfb5d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Icon +[![Buttons Online Preview]][Online Preview] [![Buttons Download]][Download] ### A multiplatform app for SIT students. @@ -34,8 +35,12 @@ The source codes and configurations are open source under [GPL v3](LICENSE). +[Online Preview]: https://liplum-dev.github.io/mimir/ + [Download]: https://github.com/liplum-dev/mimir/releases/latest [Buttons Download]: https://img.shields.io/github/downloads/liplum-dev/mimir/total?color=023a46&label=Download&logo=docusign&logoColor=white&style=for-the-badge&labelColor=034e5e + +[Buttons Online Preview]: https://img.shields.io/badge/Oneline%20Preview-2d7b7e?style=for-the-badge From 3018966b2deaecabc0a49d9d3e1ec6d295a3811c Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 9 Apr 2024 23:29:17 +0800 Subject: [PATCH 048/458] [expense] [statistics] format time span --- .../expense_records/entity/statistics.dart | 90 ++++++++++++------- lib/life/expense_records/page/statistics.dart | 24 ++--- lib/life/expense_records/utils.dart | 7 +- .../expense_records/widget/chart/bar.dart | 10 ++- lib/utils/date.dart | 19 +++- 5 files changed, 101 insertions(+), 49 deletions(-) diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index 4071e61e8..32e4a68b9 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -7,39 +7,69 @@ import 'local.dart'; typedef StartTime2Records = List<({DateTime start, List records})>; enum StatisticsMode { - week(_week), - month(_month), - year(_year); + week(), + month(), + year(); + + const StatisticsMode(); /// Resort the records, separate them by start time, and sort them in DateTime ascending order. - final StartTime2Records Function(List records) resort; + StartTime2Records resort(List records) { + switch (this) { + case StatisticsMode.week: + final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); + final startTime2Records = ym2records.entries + .map((entry) => + (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) + .toList(); + startTime2Records.sortBy((r) => r.start); + return startTime2Records; + case StatisticsMode.month: + final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.month)); + final startTime2Records = ym2records.entries + .map((entry) => (start: DateTime(entry.key.$1, entry.key.$2), records: entry.value)) + .toList(); + startTime2Records.sortBy((r) => r.start); + return startTime2Records; + case StatisticsMode.year: + final ym2records = records.groupListsBy((r) => r.timestamp.year); + final startTime2Records = + ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + startTime2Records.sortBy((r) => r.start); + return startTime2Records; + } + } - const StatisticsMode(this.resort); + DateTime getAfterUnitTime({ + required DateTime start, + DateTime? endLimit, + }) { + var end = switch (this) { + StatisticsMode.week => start.copyWith( + day: start.day + 6, + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.month => start.copyWith( + day: daysInMonth(year: start.month, month: start.month), + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.year => start.copyWith( + month: 12, + day: daysInMonth(year: start.month, month: start.month), + hour: 23, + minute: 59, + second: 59, + ) + }; + if (endLimit != null && endLimit.isBefore(end)) { + end = endLimit; + } + return end; + } String l10nName() => "expenseRecords.statsMode.$name".tr(); } - -StartTime2Records _week(List records) { - final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); - final startTime2Records = ym2records.entries - .map((entry) => (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) - .toList(); - startTime2Records.sortBy((r) => r.start); - return startTime2Records; -} - -StartTime2Records _month(List records) { - final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.month)); - final startTime2Records = - ym2records.entries.map((entry) => (start: DateTime(entry.key.$1, entry.key.$2), records: entry.value)).toList(); - startTime2Records.sortBy((r) => r.start); - return startTime2Records; -} - -StartTime2Records _year(List records) { - final ym2records = records.groupListsBy((r) => r.timestamp.year); - final startTime2Records = - ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); - startTime2Records.sortBy((r) => r.start); - return startTime2Records; -} diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index c1b4a2095..3d333ad8f 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -55,23 +55,24 @@ final _startTime2Records = Provider.autoDispose((ref) { class _ExpenseStatisticsPageState extends ConsumerState { late int index = ref.read(_startTime2Records).length - 1; final controller = ScrollController(); - var showTimeSpan = false; + final $showTimeSpan = ValueNotifier(false); @override void initState() { super.initState(); controller.addListener(() { final pos = controller.positions.last; + final showTimeSpan = $showTimeSpan.value; if (pos.pixels > pos.minScrollExtent) { if (!showTimeSpan) { setState(() { - showTimeSpan = true; + $showTimeSpan.value = true; }); } } else { if (showTimeSpan) { setState(() { - showTimeSpan = false; + $showTimeSpan.value = false; }); } } @@ -118,14 +119,15 @@ class _ExpenseStatisticsPageState extends ConsumerState { ], ), // ListView() - AnimatedSlide( - offset: showTimeSpan ? Offset.zero : const Offset(0, -2), - duration: Durations.long4, - child: AnimatedSwitcher( - duration: Durations.long4, - child: showTimeSpan ? buildHeader(current.start) : null, - ), - ).align(at: Alignment.topCenter), + $showTimeSpan >> + (ctx, showTimeSpan) => AnimatedSlide( + offset: showTimeSpan ? Offset.zero : const Offset(0, -2), + duration: Durations.long4, + child: AnimatedSwitcher( + duration: Durations.long4, + child: showTimeSpan ? buildHeader(current.start) : null, + ), + ).align(at: Alignment.topCenter), ].stack(), ); } diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 0d833d99c..e5c0c1f0b 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -152,7 +152,6 @@ String resolveTime4Display({ required DateTime date, }) { final monthFormat = DateFormat.MMMM(); - final monthDayFormat = DateFormat.Md(); final yearMonthFormat = DateFormat.yMMMM(); final yearFormat = DateFormat.y(); final now = DateTime.now(); @@ -165,13 +164,9 @@ String resolveTime4Display({ return "This week"; } else if (dateWeek == nowWeek - 1) { return "Last week"; - } else { - return "? week ${yearFormat.format(date)}"; - // return "${monthDayFormat.format(date)}"; } - } else { - return "? week ${yearFormat.format(date)}"; } + return formatTimeSpan(from: date, to: StatisticsMode.week.getAfterUnitTime(start: date)); case StatisticsMode.month: if (date.year == now.year) { if (date.month == now.month) { diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 9c90572c8..a4fdf3f36 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -42,6 +42,8 @@ class _ExpenseBarChartState extends State { ); return [ ExpenseBarChartHeader( + from: widget.start, + to: widget.mode.getAfterUnitTime(start: widget.start ,endLimit: DateTime.now()), total: delegate.total, ).padFromLTRB(16,8,0,8), AspectRatio( @@ -297,17 +299,23 @@ class AmountChartWidget extends StatelessWidget { class ExpenseBarChartHeader extends StatelessWidget { final double total; + final DateTime from; + final DateTime to; const ExpenseBarChartHeader({ super.key, required this.total, + required this.from, + required this.to, }); @override Widget build(BuildContext context) { + final labelStyle = context.textTheme.titleMedium?.copyWith(color:context.theme.disabledColor); return [ - "Total".text(style: context.textTheme.titleMedium?.copyWith(color:context.theme.disabledColor)), + "Total".text(style: labelStyle), "¥${total.toStringAsFixed(2)}".text(style: context.textTheme.titleLarge), + formatTimeSpan(from: from, to: to).text(style: labelStyle), ].column(caa: CrossAxisAlignment.start); } } diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 5f0ec35c4..83b03c35f 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -1,3 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:sit/route.dart'; + bool isLeapYear(int year) { if (year % 400 == 0) return true; if (year % 4 == 0 && year % 100 != 0) return true; @@ -73,5 +76,19 @@ String formatTimeSpan({ required DateTime from, required DateTime to, }) { - return ""; + final local = $Key.currentContext?.locale.toString(); + final year = DateFormat.y(local); + if (from.year == to.year) { + final month = DateFormat.MMMM(local); + if (from.month == to.month) { + final day = DateFormat.d(local); + return "${day.format(from)}–${day.format(to)} ${month.format(from)}, ${year.format(from)}"; + } else { + final monthDay = DateFormat.MMMMd(local); + return "${monthDay.format(from)}–${monthDay.format(to)}, ${year.format(from)}"; + } + } else { + final yearMonthDay = DateFormat.yMMMMd(local); + return "${yearMonthDay.format(from)}–${yearMonthDay.format(to)}"; + } } From 14848db501d905634d6d4b99929406d9ee9cbf41 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 01:11:30 +0800 Subject: [PATCH 049/458] [expense] [statistics] pie chart header --- lib/life/expense_records/entity/local.dart | 2 +- .../expense_records/widget/chart/bar.dart | 17 +++++---- .../expense_records/widget/chart/header.dart | 25 +++++++++++++ .../expense_records/widget/chart/pie.dart | 35 ++++++++++++++++--- 4 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 lib/life/expense_records/widget/chart/header.dart diff --git a/lib/life/expense_records/entity/local.dart b/lib/life/expense_records/entity/local.dart index 449324eee..32ead0dc4 100644 --- a/lib/life/expense_records/entity/local.dart +++ b/lib/life/expense_records/entity/local.dart @@ -124,5 +124,5 @@ enum TransactionType { required this.isConsume, }); - String localized() => "expenseRecords.type.$name".tr(); + String l10n() => "expenseRecords.type.$name".tr(); } diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index a4fdf3f36..dfb5c5f6d 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -14,6 +14,7 @@ import '../../entity/local.dart'; import '../../entity/statistics.dart'; import "../../i18n.dart"; import '../../utils.dart'; +import 'header.dart'; class ExpenseBarChart extends StatefulWidget { final DateTime start; @@ -43,9 +44,9 @@ class _ExpenseBarChartState extends State { return [ ExpenseBarChartHeader( from: widget.start, - to: widget.mode.getAfterUnitTime(start: widget.start ,endLimit: DateTime.now()), + to: widget.mode.getAfterUnitTime(start: widget.start, endLimit: DateTime.now()), total: delegate.total, - ).padFromLTRB(16,8,0,8), + ).padFromLTRB(16, 8, 0, 8), AspectRatio( aspectRatio: 1.5, child: AmountChartWidget( @@ -296,7 +297,6 @@ class AmountChartWidget extends StatelessWidget { } } - class ExpenseBarChartHeader extends StatelessWidget { final double total; final DateTime from; @@ -311,11 +311,10 @@ class ExpenseBarChartHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final labelStyle = context.textTheme.titleMedium?.copyWith(color:context.theme.disabledColor); - return [ - "Total".text(style: labelStyle), - "¥${total.toStringAsFixed(2)}".text(style: context.textTheme.titleLarge), - formatTimeSpan(from: from, to: to).text(style: labelStyle), - ].column(caa: CrossAxisAlignment.start); + return ExpenseChartHeader( + upper: "Total", + content: "¥${total.toStringAsFixed(2)}", + lower: formatTimeSpan(from: from, to: to), + ); } } diff --git a/lib/life/expense_records/widget/chart/header.dart b/lib/life/expense_records/widget/chart/header.dart new file mode 100644 index 000000000..df91e1542 --- /dev/null +++ b/lib/life/expense_records/widget/chart/header.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import 'package:rettulf/rettulf.dart'; + +class ExpenseChartHeader extends StatelessWidget { + final String upper; + final String content; + final String lower; + + const ExpenseChartHeader({ + super.key, + required this.upper, + required this.content, + required this.lower, + }); + + @override + Widget build(BuildContext context) { + final labelStyle = context.textTheme.titleMedium?.copyWith(color: context.theme.disabledColor); + return [ + upper.text(style: labelStyle), + content.text(style: context.textTheme.titleLarge), + lower.text(style: labelStyle), + ].column(caa: CrossAxisAlignment.start); + } +} diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index 0507c3401..6724b19aa 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -2,13 +2,12 @@ import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/card.dart'; import '../../entity/local.dart'; import "../../i18n.dart"; import '../../utils.dart'; +import 'header.dart'; class ExpensePieChart extends StatefulWidget { final List records; @@ -29,8 +28,13 @@ class _ExpensePieChartState extends State { Widget build(BuildContext context) { assert(widget.records.every((type) => type.isConsume)); final (:total, :parts) = separateTransactionByType(widget.records); - + final ascending = parts.entries.sortedBy((e) => e.value.total); + final atMost = ascending.last; return [ + ExpensePieChartHeader( + average: atMost.value.total / atMost.value.records.length, + type: atMost.key, + ).padFromLTRB(16, 8, 0, 0), AspectRatio( aspectRatio: 1.5, child: buildChart(parts), @@ -80,16 +84,37 @@ class _ExpensePieChartState extends State { Widget buildLegends(Map records, double total})> parts) { return parts.entries + .sortedBy((e) => -e.value.total) .map((record) { final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; final color = type.color.harmonizeWith(context.colorScheme.primary); return Chip( avatar: Icon(type.icon, color: color), labelStyle: TextStyle(color: color), - label: "${type.localized()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), + label: "${type.l10n()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), ); }) .toList() - .wrap(spacing: 4,runSpacing: 4); + .wrap(spacing: 4, runSpacing: 4); + } +} + +class ExpensePieChartHeader extends StatelessWidget { + final TransactionType type; + final double average; + + const ExpensePieChartHeader({ + super.key, + required this.type, + required this.average, + }); + + @override + Widget build(BuildContext context) { + return ExpenseChartHeader( + upper: "Average expenses", + content: "¥${average.toStringAsFixed(2)}", + lower: "for ${type.l10n()}", + ); } } From 52e0c869d537ae5c6c7b4117c9fd96747efa2daf Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 01:22:07 +0800 Subject: [PATCH 050/458] [timetable] shorter label for display mode in en --- assets/l10n/en.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 6b2ddc60d..bf38c21ae 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -73,8 +73,8 @@ timetable: title: Show wallpaper desc: Show timetable wallpaper in the screenshot displayMode: - daily: Daily - weekly: Weekly + daily: D + weekly: W weekIndexType: all: Every odd: Odd From 5a28c177046e1283775426df21b9895ae17753da Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 02:15:23 +0800 Subject: [PATCH 051/458] [settings] using riverpod in dev options --- lib/life/expense_records/utils.dart | 2 +- .../expense_records/widget/chart/bar.dart | 2 +- lib/settings/dev.dart | 5 ++ lib/settings/page/developer.dart | 61 +++++++++++-------- lib/utils/date.dart | 20 ++++-- lib/utils/hive.dart | 5 +- 6 files changed, 60 insertions(+), 35 deletions(-) diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index e5c0c1f0b..38c9745a2 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -166,7 +166,7 @@ String resolveTime4Display({ return "Last week"; } } - return formatTimeSpan(from: date, to: StatisticsMode.week.getAfterUnitTime(start: date)); + return formatDateSpan(from: date, to: StatisticsMode.week.getAfterUnitTime(start: date)); case StatisticsMode.month: if (date.year == now.year) { if (date.month == now.month) { diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index dfb5c5f6d..79d4251e7 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -314,7 +314,7 @@ class ExpenseBarChartHeader extends StatelessWidget { return ExpenseChartHeader( upper: "Total", content: "¥${total.toStringAsFixed(2)}", - lower: formatTimeSpan(from: from, to: to), + lower: formatDateSpan(from: from, to: to), ); } } diff --git a/lib/settings/dev.dart b/lib/settings/dev.dart index e407872fe..266cc63f5 100644 --- a/lib/settings/dev.dart +++ b/lib/settings/dev.dart @@ -16,6 +16,7 @@ late DevSettingsImpl Dev; class DevSettingsImpl { final Box box; + DevSettingsImpl(this.box); /// [false] by default. @@ -23,6 +24,8 @@ class DevSettingsImpl { set on(bool newV) => box.safePut(_K.on, newV); + late final $on = box.provider(_K.on); + ValueListenable listenDevMode() => box.listenable(keys: [_K.on]); /// [false] by default. @@ -32,6 +35,8 @@ class DevSettingsImpl { ValueListenable listenDemoMode() => box.listenable(keys: [_K.demoMode]); + late final $demoMode = box.provider(_K.demoMode); + List? getSavedOaCredentialsList() => (box.safeGet(_K.savedOaCredentialsList) as List?)?.cast(); diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index a6a999f4b..31e5343a7 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/init.dart'; @@ -16,16 +17,16 @@ import 'package:sit/design/widgets/navigation.dart'; import 'package:rettulf/rettulf.dart'; import '../i18n.dart'; -class DeveloperOptionsPage extends StatefulWidget { +class DeveloperOptionsPage extends ConsumerStatefulWidget { const DeveloperOptionsPage({ super.key, }); @override - State createState() => _DeveloperOptionsPageState(); + ConsumerState createState() => _DeveloperOptionsPageState(); } -class _DeveloperOptionsPageState extends State { +class _DeveloperOptionsPageState extends ConsumerState { @override Widget build(BuildContext context) { final oaCredentials = CredentialsInit.storage.oaCredentials; @@ -50,6 +51,7 @@ class _DeveloperOptionsPageState extends State { path: "/settings/developer/local-storage", ), buildReload(), + const DebugExpenseUserTile(), if (oaCredentials != null) SwitchOaUserTile( currentCredentials: oaCredentials, @@ -63,35 +65,29 @@ class _DeveloperOptionsPageState extends State { } Widget buildDevModeToggle() { - return StatefulBuilder( - builder: (ctx, setState) => ListTile( - title: i18n.dev.devMode.text(), - leading: const Icon(Icons.developer_mode_outlined), - trailing: Switch.adaptive( - value: Dev.on, - onChanged: (newV) { - setState(() { - Dev.on = newV; - }); - }, - ), + final on = ref.watch(Dev.$on) ?? false; + return ListTile( + title: i18n.dev.devMode.text(), + leading: const Icon(Icons.developer_mode_outlined), + trailing: Switch.adaptive( + value: on, + onChanged: (newV) { + ref.read(Dev.$on.notifier).set(newV); + }, ), ); } Widget buildDemoModeToggle() { - return StatefulBuilder( - builder: (ctx, setState) => ListTile( - title: i18n.dev.demoMode.text(), - trailing: Switch.adaptive( - value: Dev.demoMode, - onChanged: (newV) async { - setState(() { - Dev.demoMode = newV; - }); - await Init.initModules(); - }, - ), + final demoMode = ref.watch(Dev.$demoMode) ?? false; + return ListTile( + title: i18n.dev.demoMode.text(), + trailing: Switch.adaptive( + value: demoMode, + onChanged: (newV) async { + ref.read(Dev.$demoMode.notifier).set(newV); + await Init.initModules(); + }, ), ); } @@ -242,3 +238,14 @@ class _SwitchOaUserTileState extends State { } } } + +class DebugExpenseUserTile extends StatelessWidget { + const DebugExpenseUserTile({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: "Expense user".text(), + ); + } +} diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 83b03c35f..28fce4977 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -72,9 +72,10 @@ DateTime getDateOfFirstDayInWeek({ return DateTime(year, 1, day); } -String formatTimeSpan({ +String formatDateSpan({ required DateTime from, required DateTime to, + bool showYear = true, }) { final local = $Key.currentContext?.locale.toString(); final year = DateFormat.y(local); @@ -82,13 +83,22 @@ String formatTimeSpan({ final month = DateFormat.MMMM(local); if (from.month == to.month) { final day = DateFormat.d(local); - return "${day.format(from)}–${day.format(to)} ${month.format(from)}, ${year.format(from)}"; + return showYear + ? "${day.format(from)}–${day.format(to)} ${month.format(from)}, ${year.format(from)}" + : "${day.format(from)}–${day.format(to)} ${month.format(from)}"; } else { final monthDay = DateFormat.MMMMd(local); - return "${monthDay.format(from)}–${monthDay.format(to)}, ${year.format(from)}"; + return showYear + ? "${monthDay.format(from)}–${monthDay.format(to)}, ${year.format(from)}" + : "${monthDay.format(from)}–${monthDay.format(to)}"; } } else { - final yearMonthDay = DateFormat.yMMMMd(local); - return "${yearMonthDay.format(from)}–${yearMonthDay.format(to)}"; + if (showYear) { + final yearMonthDay = DateFormat.yMMMMd(local); + return "${yearMonthDay.format(from)}–${yearMonthDay.format(to)}"; + } else { + final monthDay = DateFormat.MMMMd(local); + return "${monthDay.format(from)}–${monthDay.format(to)}"; + } } } diff --git a/lib/utils/hive.dart b/lib/utils/hive.dart index e544b743b..d81438a8c 100644 --- a/lib/utils/hive.dart +++ b/lib/utils/hive.dart @@ -26,8 +26,9 @@ extension BoxX on Box { class BoxFieldNotifier extends StateNotifier { final Listenable listenable; final T? Function() get; + final Future Function(T? v) set; - BoxFieldNotifier(super._state, this.listenable, this.get) { + BoxFieldNotifier(super._state, this.listenable, this.get, this.set) { listenable.addListener(_refresh); } @@ -113,6 +114,7 @@ extension BoxProviderX on Box { get?.call() ?? safeGet(key), listenable(keys: [key]), () => get?.call() ?? safeGet(key), + (v) => safePut(key, v), ); }); } @@ -127,6 +129,7 @@ extension BoxProviderX on Box { get(arg), listenable(keys: [keyOf(arg)]), () => get(arg), + (v) => safePut(keyOf(arg), v), ); }); } From 0daeb1554d6fede2c7b37c80714eac464853869d Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 03:40:56 +0800 Subject: [PATCH 052/458] [settings] more riverpod --- lib/app.dart | 39 ++++------- lib/game/index.dart | 32 --------- lib/life/index.dart | 27 ++----- lib/school/exam_result/storage/result.pg.dart | 5 +- lib/school/library/storage/borrow.dart | 5 +- lib/school/widgets/campus.dart | 35 +++++----- lib/school/ywb/index.dart | 3 +- lib/school/ywb/storage/application.dart | 7 +- lib/settings/dev.dart | 5 -- lib/settings/page/about.dart | 31 +++----- lib/settings/page/index.dart | 70 +++++++------------ lib/settings/settings.dart | 23 +++--- lib/storage/hive/table.dart | 4 +- lib/utils/hive.dart | 23 +++--- 14 files changed, 109 insertions(+), 200 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 89c9474fe..a1d1be860 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -7,6 +7,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fit_system_screenshot/fit_system_screenshot.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/widgets/oa_scope.dart'; import 'package:sit/files.dart'; import 'package:sit/qrcode/handle.dart'; @@ -19,28 +20,22 @@ import 'package:sit/update/utils.dart'; import 'package:sit/utils/color.dart'; import 'package:system_theme/system_theme.dart'; -class MimirApp extends StatefulWidget { +class MimirApp extends ConsumerStatefulWidget { const MimirApp({super.key}); @override - State createState() => _MimirAppState(); + ConsumerState createState() => _MimirAppState(); } -class _MimirAppState extends State { - final $theme = Settings.theme.listenThemeChange(); +class _MimirAppState extends ConsumerState { final $routingConfig = ValueNotifier( Settings.focusTimetable ? buildTimetableFocusRouter() : buildCommonRoutingConfig(), ); - final $focusMode = Settings.listenFocusTimetable(); - final $demoMode = Dev.listenDemoMode(); late final router = buildRouter($routingConfig); @override void initState() { super.initState(); - $theme.addListener(refresh); - $demoMode.addListener(refresh); - $focusMode.addListener(refreshFocusMode); if (!kIsWeb) { fitSystemScreenshot.init(); } @@ -48,9 +43,6 @@ class _MimirAppState extends State { @override void dispose() { - $theme.removeListener(refresh); - $demoMode.removeListener(refresh); - $focusMode.removeListener(refreshFocusMode); fitSystemScreenshot.release(); super.dispose(); } @@ -64,19 +56,16 @@ class _MimirAppState extends State { super.didChangeDependencies(); } - void refresh() { - setState(() {}); - } - - void refreshFocusMode() { - $routingConfig.value = Settings.focusTimetable ? buildTimetableFocusRouter() : buildCommonRoutingConfig(); - } - @override Widget build(BuildContext context) { - final themeColor = Settings.theme.themeColorFromSystem + final demoMode = ref.watch(Dev.$demoMode) ?? false; + final themeColorFromSystem = ref.watch(Settings.theme.$themeColorFromSystem) ?? true; + ref.listen(Settings.$focusTimetable, (pre, next) { + $routingConfig.value = next ?? false ? buildTimetableFocusRouter() : buildCommonRoutingConfig(); + }); + final themeColor = themeColorFromSystem ? SystemTheme.accentColor.maybeAccent - : Settings.theme.themeColor ?? SystemTheme.accentColor.maybeAccent; + : ref.watch(Settings.theme.$themeColor) ?? SystemTheme.accentColor.maybeAccent; ThemeData bakeTheme(ThemeData origin) { return origin.copyWith( @@ -105,16 +94,17 @@ class _MimirAppState extends State { title: R.appName, onGenerateTitle: (ctx) => "appName".tr(), routerConfig: router, - debugShowCheckedModeBanner: !Dev.demoMode, + debugShowCheckedModeBanner: !demoMode, localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, - themeMode: Settings.theme.themeMode, + themeMode: ref.watch(Settings.theme.$themeMode), theme: bakeTheme(ThemeData.light()), darkTheme: bakeTheme(ThemeData.dark()), builder: (ctx, child) => OaAuthManager( child: OaOnlineManager( child: _PostServiceRunner( + key: const ValueKey("Post service runner"), child: child ?? const SizedBox(), ), ), @@ -136,6 +126,7 @@ class _PostServiceRunner extends StatefulWidget { final Widget child; const _PostServiceRunner({ + super.key, required this.child, }); diff --git a/lib/game/index.dart b/lib/game/index.dart index 8657f0a91..4bf66df51 100644 --- a/lib/game/index.dart +++ b/lib/game/index.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; import 'package:sit/game/2048/card.dart'; import 'package:sit/game/minesweeper/card.dart'; -import 'package:sit/settings/settings.dart'; import "i18n.dart"; @@ -16,35 +13,6 @@ class GamePage extends StatefulWidget { } class _GamePageState extends State { - LoginStatus? loginStatus; - final $campus = Settings.listenCampus(); - - @override - void initState() { - $campus.addListener(refresh); - super.initState(); - } - - @override - void dispose() { - $campus.removeListener(refresh); - super.dispose(); - } - - @override - void didChangeDependencies() { - final newLoginStatus = context.auth.loginStatus; - if (loginStatus != newLoginStatus) { - setState(() { - loginStatus = newLoginStatus; - }); - } - super.didChangeDependencies(); - } - - void refresh() { - setState(() {}); - } @override Widget build(BuildContext context) { diff --git a/lib/life/index.dart b/lib/life/index.dart index 5576a0af1..a24cd2392 100644 --- a/lib/life/index.dart +++ b/lib/life/index.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/login_status.dart'; import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/entity/campus.dart'; import 'package:sit/life/electricity/index.dart'; import 'package:sit/life/expense_records/index.dart'; import 'package:sit/settings/settings.dart'; @@ -10,28 +12,15 @@ import 'package:rettulf/rettulf.dart'; import 'event.dart'; import "i18n.dart"; -class LifePage extends StatefulWidget { +class LifePage extends ConsumerStatefulWidget { const LifePage({super.key}); @override - State createState() => _LifePageState(); + ConsumerState createState() => _LifePageState(); } -class _LifePageState extends State { +class _LifePageState extends ConsumerState { LoginStatus? loginStatus; - final $campus = Settings.listenCampus(); - - @override - void initState() { - $campus.addListener(refresh); - super.initState(); - } - - @override - void dispose() { - $campus.removeListener(refresh); - super.dispose(); - } @override void didChangeDependencies() { @@ -44,13 +33,9 @@ class _LifePageState extends State { super.didChangeDependencies(); } - void refresh() { - setState(() {}); - } - @override Widget build(BuildContext context) { - final campus = Settings.campus; + final campus = ref.watch(Settings.$campus) ?? Campus.fengxian; return Scaffold( resizeToAvoidBottomInset: false, body: NestedScrollView( diff --git a/lib/school/exam_result/storage/result.pg.dart b/lib/school/exam_result/storage/result.pg.dart index c199ca390..4b84d555a 100644 --- a/lib/school/exam_result/storage/result.pg.dart +++ b/lib/school/exam_result/storage/result.pg.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:sit/utils/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:sit/school/exam_result/entity/result.pg.dart'; @@ -19,7 +18,5 @@ class ExamResultPgStorage { Future setResultList(List? newV) => box.safePut(_K.resultList, newV); - ValueListenable listenResultList() => box.listenable(keys: [_K.resultList]); - - late final $resultList = box.provider>(_K.resultList, getResultList); + late final $resultList = box.provider>(_K.resultList, get: getResultList); } diff --git a/lib/school/library/storage/borrow.dart b/lib/school/library/storage/borrow.dart index d751f1c50..9759109d6 100644 --- a/lib/school/library/storage/borrow.dart +++ b/lib/school/library/storage/borrow.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:sit/utils/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:sit/storage/hive/init.dart'; @@ -20,9 +19,7 @@ class LibraryBorrowStorage { Future setBorrowedBooks(List? value) => box.safePut(_K.borrowed, value); - late final $borrowed = box.provider>(_K.borrowed, getBorrowedBooks); - - Listenable listenBorrowedBooks() => box.listenable(keys: [_K.borrowed]); + late final $borrowed = box.provider>(_K.borrowed, get: getBorrowedBooks); List? getBorrowHistory() => (box.safeGet(_K.borrowHistory) as List?)?.cast(); diff --git a/lib/school/widgets/campus.dart b/lib/school/widgets/campus.dart index 62caa6d08..8cad2e1c4 100644 --- a/lib/school/widgets/campus.dart +++ b/lib/school/widgets/campus.dart @@ -1,32 +1,29 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/entity/campus.dart'; import 'package:sit/settings/settings.dart'; import 'package:rettulf/rettulf.dart'; -class CampusSelector extends StatelessWidget { +class CampusSelector extends ConsumerWidget { const CampusSelector({super.key}); @override - Widget build(BuildContext context) { - return StatefulBuilder( - builder: (ctx, setState) => SegmentedButton( - segments: Campus.values - .map((e) => ButtonSegment( - icon: Icon(context.icons.location), - value: e, - label: e.l10nName().text(), - )) - .toList(), - selected: {Settings.campus}, - onSelectionChanged: (newSelection) async { - setState(() { - Settings.campus = newSelection.first; - }); - await HapticFeedback.mediumImpact(); - }, - ), + Widget build(BuildContext context, WidgetRef ref) { + return SegmentedButton( + segments: Campus.values + .map((e) => ButtonSegment( + icon: Icon(context.icons.location), + value: e, + label: e.l10nName().text(), + )) + .toList(), + selected: {ref.watch(Settings.$campus) ?? Campus.fengxian}, + onSelectionChanged: (newSelection) async { + ref.read(Settings.$campus.notifier).set(newSelection.first); + await HapticFeedback.mediumImpact(); + }, ); } } diff --git a/lib/school/ywb/index.dart b/lib/school/ywb/index.dart index c875067a7..6cda1d583 100644 --- a/lib/school/ywb/index.dart +++ b/lib/school/ywb/index.dart @@ -25,7 +25,8 @@ class _YwbAppCardState extends ConsumerState { @override Widget build(BuildContext context) { final storage = YwbInit.applicationStorage; - final running = ref.watch(storage.$applicationFamily(YwbApplicationType.running)); + final family = storage.$applicationFamily(YwbApplicationType.running); + final running = ref.watch(family); return AppCard( title: i18n.title.text(), view: running == null ? null : buildRunningCard(running), diff --git a/lib/school/ywb/storage/application.dart b/lib/school/ywb/storage/application.dart index 8017eaa39..1506c1c9b 100644 --- a/lib/school/ywb/storage/application.dart +++ b/lib/school/ywb/storage/application.dart @@ -24,6 +24,9 @@ class YwbApplicationStorage { Listenable listenApplicationListOf(YwbApplicationType type) => box.listenable(keys: [_K.applicationListOf(type)]); - late final $applicationFamily = - box.providerFamily, YwbApplicationType>(_K.applicationListOf, getApplicationListOf); + late final $applicationFamily = box.providerFamily, YwbApplicationType>( + _K.applicationListOf, + get: getApplicationListOf, + set: setApplicationListOf, + ); } diff --git a/lib/settings/dev.dart b/lib/settings/dev.dart index 266cc63f5..4f1a215d3 100644 --- a/lib/settings/dev.dart +++ b/lib/settings/dev.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:sit/utils/hive.dart'; import 'package:sit/credentials/entity/credential.dart'; @@ -26,15 +25,11 @@ class DevSettingsImpl { late final $on = box.provider(_K.on); - ValueListenable listenDevMode() => box.listenable(keys: [_K.on]); - /// [false] by default. bool get demoMode => box.safeGet(_K.demoMode) ?? false; set demoMode(bool newV) => box.safePut(_K.demoMode, newV); - ValueListenable listenDemoMode() => box.listenable(keys: [_K.demoMode]); - late final $demoMode = box.provider(_K.demoMode); List? getSavedOaCredentialsList() => diff --git a/lib/settings/page/about.dart b/lib/settings/page/about.dart index 753a6e529..d4bfbda87 100644 --- a/lib/settings/page/about.dart +++ b/lib/settings/page/about.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/widgets/list_tile.dart'; @@ -90,35 +91,19 @@ class _AboutSettingsPageState extends State { } } -class VersionTile extends StatefulWidget { +class VersionTile extends ConsumerStatefulWidget { const VersionTile({super.key}); @override - State createState() => _VersionTileState(); + ConsumerState createState() => _VersionTileState(); } -class _VersionTileState extends State { +class _VersionTileState extends ConsumerState { int clickCount = 0; - final $isDeveloperMode = Dev.listenDevMode(); - - @override - void initState() { - super.initState(); - $isDeveloperMode.addListener(refresh); - } - - @override - void dispose() { - $isDeveloperMode.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } @override Widget build(BuildContext context) { + final devOn = ref.watch(Dev.$on) ?? false; final version = R.currentVersion; return ListTile( leading: switch (version.platform) { @@ -132,15 +117,15 @@ class _VersionTileState extends State { title: i18n.about.version.text(), subtitle: "${version.platform.name} ${version.version.toString()}".text(), trailing: kIsWeb ? null : const CheckUpdateButton(), - onTap: Dev.on && clickCount <= 10 + onTap: devOn && clickCount <= 10 ? null : () async { - if (Dev.on) return; + if (ref.read(Dev.$on) ?? false) return; clickCount++; if (clickCount >= 10) { clickCount = 0; - Dev.on = true; context.showSnackBar(content: i18n.dev.devModeActivateTip.text()); + await ref.read(Dev.$on.notifier).set(true); await HapticFeedback.mediumImpact(); } }, diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index 29de27653..a43077b1b 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/login_status.dart'; import 'package:sit/credentials/widgets/oa_scope.dart'; @@ -22,32 +23,14 @@ import 'package:locale_names/locale_names.dart'; import '../i18n.dart'; import '../../design/widgets/navigation.dart'; -class SettingsPage extends StatefulWidget { +class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); @override - State createState() => _SettingsPageState(); + ConsumerState createState() => _SettingsPageState(); } -class _SettingsPageState extends State { - final $isDeveloperMode = Dev.listenDevMode(); - - @override - void initState() { - super.initState(); - $isDeveloperMode.addListener(refresh); - } - - @override - void dispose() { - $isDeveloperMode.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - +class _SettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { return Scaffold( @@ -69,6 +52,7 @@ class _SettingsPageState extends State { } List buildEntries() { + final devOn = ref.watch(Dev.$on) ?? false; final all = []; final auth = context.auth; if (auth.loginStatus != LoginStatus.never) { @@ -129,7 +113,7 @@ class _SettingsPageState extends State { } all.add(const Divider()); } - if (Dev.on) { + if (devOn) { all.add(PageNavigationTile( title: i18n.dev.title.text(), leading: const Icon(Icons.developer_mode_outlined), @@ -159,27 +143,27 @@ class _SettingsPageState extends State { } Widget buildThemeMode() { - return Settings.theme.listenThemeMode() >> - (ctx, _) => ListTile( - leading: switch (Settings.theme.themeMode) { - ThemeMode.dark => const Icon(Icons.dark_mode), - ThemeMode.light => const Icon(Icons.light_mode), - ThemeMode.system => const Icon(Icons.brightness_auto), - }, - isThreeLine: true, - title: i18n.themeModeTitle.text(), - subtitle: ThemeMode.values - .map((mode) => ChoiceChip( - label: mode.l10n().text(), - selected: Settings.theme.themeMode == mode, - onSelected: (value) async { - Settings.theme.themeMode = mode; - await HapticFeedback.mediumImpact(); - }, - )) - .toList() - .wrap(spacing: 4), - ); + final themeMode = ref.watch(Settings.theme.$themeMode) ?? ThemeMode.system; + return ListTile( + leading: switch (themeMode) { + ThemeMode.dark => const Icon(Icons.dark_mode), + ThemeMode.light => const Icon(Icons.light_mode), + ThemeMode.system => const Icon(Icons.brightness_auto), + }, + isThreeLine: true, + title: i18n.themeModeTitle.text(), + subtitle: ThemeMode.values + .map((mode) => ChoiceChip( + label: mode.l10n().text(), + selected: Settings.theme.themeMode == mode, + onSelected: (value) async { + ref.read(Settings.theme.$themeMode.notifier).set(mode); + await HapticFeedback.mediumImpact(); + }, + )) + .toList() + .wrap(spacing: 4), + ); } } diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index bb43a5afd..35977c1cc 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -1,5 +1,4 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:sit/utils/hive.dart'; @@ -42,13 +41,13 @@ class SettingsImpl { set campus(Campus newV) => box.safePut(_K.campus, newV); - ValueListenable listenCampus() => box.listenable(keys: [_K.campus]); + late final $campus = box.provider(_K.campus); bool get focusTimetable => box.safeGet(_K.focusTimetable) ?? false; set focusTimetable(bool newV) => box.safePut(_K.focusTimetable, newV); - ValueListenable listenFocusTimetable() => box.listenable(keys: [_K.focusTimetable]); + late final $focusTimetable = box.provider(_K.focusTimetable); String? get lastSignature => box.safeGet(_K.lastSignature); @@ -69,7 +68,7 @@ class _ThemeK { class _Theme { final Box box; - const _Theme(this.box); + _Theme(this.box); // theme Color? get themeColor { @@ -85,22 +84,24 @@ class _Theme { box.safePut(_ThemeK.themeColor, v?.value); } + late final $themeColor = box.provider( + _ThemeK.themeColor, + get: () => themeColor, + set: (v) => themeColor = v, + ); + bool get themeColorFromSystem => box.safeGet(_ThemeK.themeColorFromSystem) ?? true; set themeColorFromSystem(bool value) => box.safePut(_ThemeK.themeColorFromSystem, value); + late final $themeColorFromSystem = box.provider(_ThemeK.themeColorFromSystem); + /// [ThemeMode.system] by default. ThemeMode get themeMode => box.safeGet(_ThemeK.themeMode) ?? ThemeMode.system; set themeMode(ThemeMode value) => box.safePut(_ThemeK.themeMode, value); - ValueListenable listenThemeMode() => box.listenable(keys: [_ThemeK.themeMode]); - - ValueListenable listenThemeChange() => box.listenable(keys: [ - _ThemeK.themeMode, - _ThemeK.themeColor, - _ThemeK.themeColorFromSystem, - ]); + late final $themeMode = box.provider(_ThemeK.themeMode); } enum ProxyType { diff --git a/lib/storage/hive/table.dart b/lib/storage/hive/table.dart index eb5c13876..65c371d7c 100644 --- a/lib/storage/hive/table.dart +++ b/lib/storage/hive/table.dart @@ -188,11 +188,11 @@ class HiveTable { } AutoDisposeStateNotifierProviderFamily, T?, int> rowProviderFamily(int id) { - return box.providerFamily(_rowK, get); + return box.providerFamily(_rowK, get: get); } late final selectedRowProvider = box.provider( _selectedIdK, - () => selectedRow, + get: () => selectedRow, ); } diff --git a/lib/utils/hive.dart b/lib/utils/hive.dart index d81438a8c..0b37600ad 100644 --- a/lib/utils/hive.dart +++ b/lib/utils/hive.dart @@ -26,7 +26,7 @@ extension BoxX on Box { class BoxFieldNotifier extends StateNotifier { final Listenable listenable; final T? Function() get; - final Future Function(T? v) set; + final FutureOr Function(T? v) set; BoxFieldNotifier(super._state, this.listenable, this.get, this.set) { listenable.addListener(_refresh); @@ -108,28 +108,33 @@ class BoxFieldStreamNotifier extends StateNotifier { extension BoxProviderX on Box { /// For generic class, like [List] or [Map], please specify the [get] for type conversion. - AutoDisposeStateNotifierProvider, T?> provider(dynamic key, [T? Function()? get]) { + AutoDisposeStateNotifierProvider, T?> provider( + dynamic key, { + T? Function()? get, + FutureOr Function(T? v)? set, + }) { return StateNotifierProvider.autoDispose, T?>((ref) { return BoxFieldNotifier( get?.call() ?? safeGet(key), listenable(keys: [key]), () => get?.call() ?? safeGet(key), - (v) => safePut(key, v), + (v) => set?.call(v) ?? safePut(key, v), ); }); } /// For generic class, like [List] or [Map], please specify the [get] for type conversion. AutoDisposeStateNotifierProviderFamily, T?, Arg> providerFamily( - dynamic Function(Arg arg) keyOf, - T? Function(Arg arg) get, - ) { + dynamic Function(Arg arg) keyOf, { + T? Function(Arg arg)? get, + FutureOr Function(Arg arg, T? v)? set, + }) { return StateNotifierProvider.autoDispose.family, T?, Arg>((ref, arg) { return BoxFieldNotifier( - get(arg), + get?.call(arg) ?? safeGet(arg), listenable(keys: [keyOf(arg)]), - () => get(arg), - (v) => safePut(keyOf(arg), v), + () => get?.call(arg) ?? safeGet(arg), + (v) => set?.call(arg, v) ?? safePut(arg, v), ); }); } From 92c2dd16d67a0d8c1e829dd1e3d8981b2905d739 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 05:04:47 +0800 Subject: [PATCH 053/458] [credentials] riverpod --- lib/app.dart | 11 ++- lib/credentials/storage/credential.dart | 22 ++---- lib/credentials/widgets/oa_scope.dart | 81 ---------------------- lib/life/expense_records/index.dart | 8 +-- lib/life/expense_records/page/records.dart | 8 +-- lib/life/index.dart | 16 +---- lib/login/page/index.dart | 25 +++---- lib/me/edu_email/page/inbox.dart | 40 ++++------- lib/me/widgets/greeting.dart | 63 ++++++----------- lib/route.dart | 12 ++-- lib/school/class2nd/index.dart | 6 +- lib/school/class2nd/page/attended.dart | 12 ++-- lib/school/exam_arrange/page/list.dart | 12 ++-- lib/school/exam_result/page/result.ug.dart | 12 ++-- lib/school/index.dart | 27 ++------ lib/school/oa_announce/page/list.dart | 12 ++-- lib/settings/page/credentials.dart | 10 +-- lib/settings/page/index.dart | 16 ++--- lib/settings/page/school.dart | 24 ++----- lib/timetable/page/import.dart | 20 +++--- lib/utils/hive.dart | 39 +++++++++++ 21 files changed, 182 insertions(+), 294 deletions(-) delete mode 100644 lib/credentials/widgets/oa_scope.dart diff --git a/lib/app.dart b/lib/app.dart index a1d1be860..c1b3451f2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,7 +8,6 @@ import 'package:fit_system_screenshot/fit_system_screenshot.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; import 'package:sit/files.dart'; import 'package:sit/qrcode/handle.dart'; import 'package:sit/r.dart'; @@ -101,12 +100,10 @@ class _MimirAppState extends ConsumerState { themeMode: ref.watch(Settings.theme.$themeMode), theme: bakeTheme(ThemeData.light()), darkTheme: bakeTheme(ThemeData.dark()), - builder: (ctx, child) => OaAuthManager( - child: OaOnlineManager( - child: _PostServiceRunner( - key: const ValueKey("Post service runner"), - child: child ?? const SizedBox(), - ), + builder: (ctx, child) => OaOnlineManager( + child: _PostServiceRunner( + key: const ValueKey("Post service runner"), + child: child ?? const SizedBox(), ), ), scrollBehavior: const MaterialScrollBehavior().copyWith( diff --git a/lib/credentials/storage/credential.dart b/lib/credentials/storage/credential.dart index 48eee6116..625449bb8 100644 --- a/lib/credentials/storage/credential.dart +++ b/lib/credentials/storage/credential.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:sit/credentials/entity/user_type.dart'; import 'package:sit/storage/hive/init.dart'; @@ -35,34 +34,31 @@ class CredentialStorage { set oaCredentials(Credentials? newV) => box.safePut(_OaK.credentials, newV); + late final $oaCredentials = box.provider(_OaK.credentials); + DateTime? get oaLastAuthTime => box.safeGet(_OaK.lastAuthTime); set oaLastAuthTime(DateTime? newV) => box.safePut(_OaK.lastAuthTime, newV); + late final $oaLastAuthTime = box.provider(_OaK.lastAuthTime); + LoginStatus? get oaLoginStatus => box.safeGet(_OaK.loginStatus); set oaLoginStatus(LoginStatus? newV) => box.safePut(_OaK.loginStatus, newV); + late final $oaLoginStatus = box.providerWithDefault(_OaK.loginStatus, () => LoginStatus.never); + OaUserType? get oaUserType => box.safeGet(_OaK.userType); set oaUserType(OaUserType? newV) => box.safePut(_OaK.userType, newV); - ValueListenable listenOaChange() => box.listenable(keys: [ - _OaK.credentials, - _OaK.lastAuthTime, - _OaK.loginStatus, - _OaK.userType, - ]); + late final $oaUserType = box.provider(_OaK.userType); // Edu Email Credentials? get eduEmailCredentials => box.safeGet(_EmailK.credentials); set eduEmailCredentials(Credentials? newV) => box.safePut(_EmailK.credentials, newV); - ValueListenable listenEduEmailChange() => box.listenable(keys: [ - _EmailK.credentials, - ]); - late final $eduEmailCredentials = box.provider(_EmailK.credentials); // Library @@ -70,9 +66,5 @@ class CredentialStorage { set libraryCredentials(Credentials? newV) => box.safePut(_LibraryK.credentials, newV); - ValueListenable listenLibraryChange() => box.listenable(keys: [ - _LibraryK.credentials, - ]); - late final $libraryCredentials = box.provider(_LibraryK.credentials); } diff --git a/lib/credentials/widgets/oa_scope.dart b/lib/credentials/widgets/oa_scope.dart deleted file mode 100644 index 6673036c6..000000000 --- a/lib/credentials/widgets/oa_scope.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/entity/user_type.dart'; - -import '../entity/login_status.dart'; -import '../init.dart'; - -extension OaAuthX on BuildContext { - OaAuth get auth => OaAuth.of(this); -} - -class OaAuthManager extends StatefulWidget { - final Widget child; - - const OaAuthManager({super.key, required this.child}); - - @override - State createState() => _OaAuthManagerState(); -} - -class _OaAuthManagerState extends State { - final onOaChanged = CredentialsInit.storage.listenOaChange(); - - @override - void initState() { - super.initState(); - onOaChanged.addListener(anyChange); - } - - @override - void dispose() { - onOaChanged.removeListener(anyChange); - super.dispose(); - } - - void anyChange() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final storage = CredentialsInit.storage; - return OaAuth( - credentials: storage.oaCredentials, - lastAuthTime: storage.oaLastAuthTime, - loginStatus: storage.oaLoginStatus ?? LoginStatus.never, - userType: storage.oaUserType ?? OaUserType.other, - child: widget.child, - ); - } -} - -class OaAuth extends InheritedWidget { - final Credentials? credentials; - final DateTime? lastAuthTime; - final LoginStatus loginStatus; - final OaUserType userType; - - const OaAuth({ - super.key, - this.credentials, - this.lastAuthTime, - required this.loginStatus, - required this.userType, - required super.child, - }); - - static OaAuth of(BuildContext context) { - final OaAuth? result = context.dependOnInheritedWidgetOfExactType(); - assert(result != null, 'No AuthScope found in context'); - return result!; - } - - @override - bool updateShouldNotify(OaAuth oldWidget) { - return credentials != oldWidget.credentials || - lastAuthTime != oldWidget.lastAuthTime || - loginStatus != oldWidget.loginStatus || - userType != oldWidget.userType; - } -} diff --git a/lib/life/expense_records/index.dart b/lib/life/expense_records/index.dart index 61502d405..7023442ce 100644 --- a/lib/life/expense_records/index.dart +++ b/lib/life/expense_records/index.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/widgets/app.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/l10n/extension.dart'; @@ -55,11 +55,11 @@ class _ExpenseRecordsAppCardState extends ConsumerState { } Future refresh({required bool active}) async { - final oaCredential = context.auth.credentials; - if (oaCredential == null) return; + final credentials = ref.read(CredentialsInit.storage.$oaCredentials); + if (credentials == null) return; try { await ExpenseAggregated.fetchAndSaveTransactionUntilNow( - studentId: oaCredential.account, + studentId: credentials.account, ); } catch (error) { if (active) { diff --git a/lib/life/expense_records/page/records.dart b/lib/life/expense_records/page/records.dart index ed83d1f72..cbd627421 100644 --- a/lib/life/expense_records/page/records.dart +++ b/lib/life/expense_records/page/records.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:sit/life/expense_records/storage/local.dart'; @@ -44,15 +44,15 @@ class _ExpenseRecordsPageState extends ConsumerState { } Future refresh() async { - final oaCredential = context.auth.credentials; - if (oaCredential == null) return; + final credentials = ref.read(CredentialsInit.storage.$oaCredentials); + if (credentials == null) return; if (!mounted) return; setState(() { isFetching = true; }); try { await ExpenseAggregated.fetchAndSaveTransactionUntilNow( - studentId: oaCredential.account, + studentId: credentials.account, ); updateRecords(ExpenseRecordsInit.storage.getTransactionsByRange()); setState(() { diff --git a/lib/life/index.dart b/lib/life/index.dart index a24cd2392..373294bf6 100644 --- a/lib/life/index.dart +++ b/lib/life/index.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/entity/campus.dart'; import 'package:sit/life/electricity/index.dart'; import 'package:sit/life/expense_records/index.dart'; @@ -20,21 +20,9 @@ class LifePage extends ConsumerStatefulWidget { } class _LifePageState extends ConsumerState { - LoginStatus? loginStatus; - - @override - void didChangeDependencies() { - final newLoginStatus = context.auth.loginStatus; - if (loginStatus != newLoginStatus) { - setState(() { - loginStatus = newLoginStatus; - }); - } - super.didChangeDependencies(); - } - @override Widget build(BuildContext context) { + final loginStatus = ref.watch(CredentialsInit.storage.$oaLoginStatus); final campus = ref.watch(Settings.$campus) ?? Campus.fengxian; return Scaffold( resizeToAvoidBottomInset: false, diff --git a/lib/login/page/index.dart b/lib/login/page/index.dart index 8cc7f79b3..dff174f4d 100644 --- a/lib/login/page/index.dart +++ b/lib/login/page/index.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; @@ -10,7 +11,6 @@ import 'package:sit/credentials/entity/login_status.dart'; import 'package:sit/credentials/entity/user_type.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/credentials/utils.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/init.dart'; @@ -31,16 +31,16 @@ const i18n = OaLoginI18n(); const _forgotLoginPasswordUrl = "https://authserver.sit.edu.cn/authserver/getBackPasswordMainPage.do?service=https%3A%2F%2Fmyportal.sit.edu.cn%3A443%2F"; -class LoginPage extends StatefulWidget { +class LoginPage extends ConsumerStatefulWidget { final bool isGuarded; const LoginPage({super.key, required this.isGuarded}); @override - State createState() => _LoginPageState(); + ConsumerState createState() => _LoginPageState(); } -class _LoginPageState extends State { +class _LoginPageState extends ConsumerState { final $account = TextEditingController(text: Dev.demoMode ? R.demoModeOaAccount : null); final $password = TextEditingController(text: Dev.demoMode ? R.demoModeOaPassword : null); final _formKey = GlobalKey(); @@ -69,16 +69,6 @@ class _LoginPageState extends State { } } - @override - void didChangeDependencies() { - final oaCredential = context.auth.credentials; - if (oaCredential != null) { - $account.text = oaCredential.account; - $password.text = oaCredential.password; - } - super.didChangeDependencies(); - } - /// 用户点击登录按钮后 Future login() async { final account = $account.text; @@ -155,6 +145,13 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { + ref.listen(CredentialsInit.storage.$oaCredentials, (pre, next) { + if (next != null) { + $account.text = next.account; + $password.text = next.password; + } + }); + return GestureDetector( onTap: () { // dismiss the keyboard when tap out of TextField. diff --git a/lib/me/edu_email/page/inbox.dart b/lib/me/edu_email/page/inbox.dart index 57fab1a0b..a995c350c 100644 --- a/lib/me/edu_email/page/inbox.dart +++ b/lib/me/edu_email/page/inbox.dart @@ -1,5 +1,6 @@ import 'package:enough_mail/enough_mail.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/init.dart'; import 'package:rettulf/rettulf.dart'; @@ -10,47 +11,29 @@ import '../i18n.dart'; import '../widgets/item.dart'; // TODO: Send email -class EduEmailInboxPage extends StatefulWidget { +class EduEmailInboxPage extends ConsumerStatefulWidget { const EduEmailInboxPage({super.key}); @override - State createState() => _EduEmailInboxPageState(); + ConsumerState createState() => _EduEmailInboxPageState(); } -class _EduEmailInboxPageState extends State { +class _EduEmailInboxPageState extends ConsumerState { List? messages; - Credentials? credential = CredentialsInit.storage.eduEmailCredentials; - final onEduEmailChanged = CredentialsInit.storage.listenEduEmailChange(); @override - void initState() { + initState() { super.initState(); - onEduEmailChanged.addListener(updateCredential); - refresh(); - } - - @override - void dispose() { - onEduEmailChanged.removeListener(updateCredential); - super.dispose(); - } - - void updateCredential() { - final newCredential = CredentialsInit.storage.eduEmailCredentials; - setState(() { - credential = newCredential; - }); - if (newCredential != null) { - refresh(); + final credentials = ref.read(CredentialsInit.storage.$eduEmailCredentials); + if (credentials != null) { + refresh(credentials); } } - Future refresh() async { - final credential = this.credential; - if (credential == null) return; + Future refresh(Credentials credentials) async { if (!mounted) return; try { - await EduEmailInit.service.login(credential); + await EduEmailInit.service.login(credentials); } catch (error, stackTrace) { handleRequestError(context, error, stackTrace); CredentialsInit.storage.eduEmailCredentials = null; @@ -74,6 +57,7 @@ class _EduEmailInboxPageState extends State { @override Widget build(BuildContext context) { + final credentials = ref.watch(CredentialsInit.storage.$eduEmailCredentials); final messages = this.messages; return Scaffold( body: CustomScrollView( @@ -81,7 +65,7 @@ class _EduEmailInboxPageState extends State { SliverAppBar( floating: true, title: i18n.inbox.title.text(), - bottom: credential != null && messages == null + bottom: credentials != null && messages == null ? const PreferredSize( preferredSize: Size.fromHeight(4), child: LinearProgressIndicator(), diff --git a/lib/me/widgets/greeting.dart b/lib/me/widgets/greeting.dart index 7ec3ce1d5..7d041c047 100644 --- a/lib/me/widgets/greeting.dart +++ b/lib/me/widgets/greeting.dart @@ -2,24 +2,21 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/entity/campus.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sit/credentials/entity/credential.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/l10n/common.dart'; -import 'package:sit/settings/settings.dart'; import 'package:sit/utils/timer.dart'; import 'package:rettulf/rettulf.dart'; -class Greeting extends StatefulWidget { +class Greeting extends ConsumerStatefulWidget { const Greeting({super.key}); @override - State createState() => _GreetingState(); + ConsumerState createState() => _GreetingState(); } -class _GreetingState extends State { - int? studyDays; - Campus campus = Settings.campus; - +class _GreetingState extends ConsumerState { Timer? dayWatcher; DateTime? _admissionDate; @@ -31,46 +28,29 @@ class _GreetingState extends State { dayWatcher = runPeriodically(const Duration(minutes: 1), (timer) { final admissionDate = _admissionDate; if (admissionDate != null) { - final now = DateTime.now(); - setState(() { - studyDays = now.difference(admissionDate).inDays; - }); + setState(() {}); } }); } - @override - void didChangeDependencies() { - // 如果用户不是新生或老师,那么就显示学习天数 - if (context.auth.credentials != null) { - setState(() { - studyDays = _getStudyDaysAndInitState(); - }); - } - super.didChangeDependencies(); - } - @override void dispose() { dayWatcher?.cancel(); super.dispose(); } - int _getStudyDaysAndInitState() { - final oaCredential = context.auth.credentials; - if (oaCredential != null) { - final id = oaCredential.account; - - if (id.isNotEmpty) { - final admissionYearTrailing = int.tryParse(id.substring(0, 2)); - if (admissionYearTrailing != null) { - int admissionYear = 2000 + admissionYearTrailing; - final admissionDate = DateTime(admissionYear, 9, 1); - _admissionDate = admissionDate; - - /// 计算入学时间, 默认按 9 月 1 日开学来算. 年份 admissionYear 是完整的年份, 如 2018. - return DateTime.now().difference(admissionDate).inDays; - } + int _getStudyDaysAndInitState(Credentials credentials) { + final id = credentials.account; + if (id.isNotEmpty) { + final admissionYearTrailing = int.tryParse(id.substring(0, 2)); + if (admissionYearTrailing != null) { + int admissionYear = 2000 + admissionYearTrailing; + final admissionDate = DateTime(admissionYear, 9, 1); + _admissionDate = admissionDate; + + /// To calculate admission year after 2000, the default start date is September 1st. + /// e.g. 2018. + return DateTime.now().difference(admissionDate).inDays; } } return 0; @@ -78,12 +58,15 @@ class _GreetingState extends State { @override Widget build(BuildContext context) { + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); + final studyDays = credentials == null ? 0 : _getStudyDaysAndInitState(credentials); + final days = studyDays; return ListTile( titleTextStyle: context.textTheme.titleMedium, title: _i18n.headerA.text(), subtitleTextStyle: context.textTheme.headlineSmall, - subtitle: _i18n.headerB((days ?? 0) + 1).text(), + subtitle: _i18n.headerB((days) + 1).text(), ); } } diff --git a/lib/route.dart b/lib/route.dart index a82296053..d8b036a5a 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/game/2048/index.dart'; import 'package:sit/game/index.dart'; import 'package:sit/game/minesweeper/index.dart'; @@ -75,8 +76,9 @@ final $MeShellKey = GlobalKey(); bool isLoginGuarded(BuildContext ctx) { if (Dev.demoMode) return false; - final auth = ctx.auth; - return auth.loginStatus != LoginStatus.validated && auth.credentials == null; + final loginStatus = ProviderScope.containerOf(ctx).read(CredentialsInit.storage.$oaLoginStatus); + final credentials = ProviderScope.containerOf(ctx).read(CredentialsInit.storage.$oaCredentials); + return loginStatus != LoginStatus.validated && credentials == null; } String? _loginRequired(BuildContext ctx, GoRouterState state) { @@ -85,8 +87,8 @@ String? _loginRequired(BuildContext ctx, GoRouterState state) { } FutureOr _redirectRoot(BuildContext ctx, GoRouterState state) { - final auth = ctx.auth; - if (auth.loginStatus == LoginStatus.never) { + final loginStatus = ProviderScope.containerOf(ctx).read(CredentialsInit.storage.$oaLoginStatus); + if (loginStatus == LoginStatus.never) { // allow to access settings page. if (state.matchedLocation.startsWith("/tools")) return null; if (state.matchedLocation.startsWith("/settings")) return null; diff --git a/lib/school/class2nd/index.dart b/lib/school/class2nd/index.dart index d7e09b35d..e27858d06 100644 --- a/lib/school/class2nd/index.dart +++ b/lib/school/class2nd/index.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' hide isCupertino; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/app.dart'; import 'package:sit/design/adaptive/dialog.dart'; @@ -73,7 +72,8 @@ class _Class2ndAppCardState extends ConsumerState { } Class2ndPointsSummary getTargetScore() { - final admissionYear = getAdmissionYearFromStudentId(context.auth.credentials?.account); + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); + final admissionYear = getAdmissionYearFromStudentId(credentials?.account); return getTargetScoreOf(admissionYear: admissionYear); } diff --git a/lib/school/class2nd/page/attended.dart b/lib/school/class2nd/page/attended.dart index 53edafeca..8c7af73a5 100644 --- a/lib/school/class2nd/page/attended.dart +++ b/lib/school/class2nd/page/attended.dart @@ -2,8 +2,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; @@ -23,14 +24,14 @@ import '../entity/attended.dart'; import '../init.dart'; import '../i18n.dart'; -class AttendedActivityPage extends StatefulWidget { +class AttendedActivityPage extends ConsumerStatefulWidget { const AttendedActivityPage({super.key}); @override - State createState() => _AttendedActivityPageState(); + ConsumerState createState() => _AttendedActivityPageState(); } -class _AttendedActivityPageState extends State { +class _AttendedActivityPageState extends ConsumerState { List? attended = () { final applications = Class2ndInit.pointStorage.applicationList; final scores = Class2ndInit.pointStorage.pointItemList; @@ -84,7 +85,8 @@ class _AttendedActivityPageState extends State { } Class2ndPointsSummary getTargetScore() { - final admissionYear = int.tryParse(context.auth.credentials?.account.substring(0, 2) ?? "") ?? 2000; + final credentials = ref.read(CredentialsInit.storage.$oaCredentials); + final admissionYear = int.tryParse(credentials?.account.substring(0, 2) ?? "") ?? 2000; return getTargetScoreOf(admissionYear: admissionYear); } diff --git a/lib/school/exam_arrange/page/list.dart b/lib/school/exam_arrange/page/list.dart index 3e4a85d7b..050497c11 100644 --- a/lib/school/exam_arrange/page/list.dart +++ b/lib/school/exam_arrange/page/list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:sit/school/entity/school.dart'; @@ -13,14 +14,14 @@ import '../i18n.dart'; import '../init.dart'; import '../widgets/exam.dart'; -class ExamArrangementListPage extends StatefulWidget { +class ExamArrangementListPage extends ConsumerStatefulWidget { const ExamArrangementListPage({super.key}); @override - State createState() => _ExamArrangementListPageState(); + ConsumerState createState() => _ExamArrangementListPageState(); } -class _ExamArrangementListPageState extends State { +class _ExamArrangementListPageState extends ConsumerState { List? examList; bool isFetching = false; late SemesterInfo initial = ExamArrangeInit.storage.lastSemesterInfo ?? estimateCurrentSemester(); @@ -103,9 +104,10 @@ class _ExamArrangementListPageState extends State { } Widget buildSemesterSelector() { + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); return SemesterSelector( initial: initial, - baseYear: getAdmissionYearFromStudentId(context.auth.credentials?.account), + baseYear: getAdmissionYearFromStudentId(credentials?.account), onSelected: (newSelection) { setState(() { selected = newSelection; diff --git a/lib/school/exam_result/page/result.ug.dart b/lib/school/exam_result/page/result.ug.dart index 5fa7cf8da..09c166856 100644 --- a/lib/school/exam_result/page/result.ug.dart +++ b/lib/school/exam_result/page/result.ug.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/animation/progress.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/common.dart'; @@ -20,14 +21,14 @@ import '../widgets/ug.dart'; import '../i18n.dart'; import 'evaluation.dart'; -class ExamResultUgPage extends StatefulWidget { +class ExamResultUgPage extends ConsumerStatefulWidget { const ExamResultUgPage({super.key}); @override - State createState() => _ExamResultUgPageState(); + ConsumerState createState() => _ExamResultUgPageState(); } -class _ExamResultUgPageState extends State { +class _ExamResultUgPageState extends ConsumerState { late SemesterInfo initial = ExamResultInit.ugStorage.lastSemesterInfo ?? estimateCurrentSemester(); late List? resultList = ExamResultInit.ugStorage.getResultList(initial); bool isFetching = false; @@ -132,9 +133,10 @@ class _ExamResultUgPageState extends State { } Widget buildSemesterSelector() { + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); return SemesterSelector( initial: initial, - baseYear: getAdmissionYearFromStudentId(context.auth.credentials?.account), + baseYear: getAdmissionYearFromStudentId(credentials?.account), onSelected: (newSelection) { ExamResultInit.ugStorage.lastSemesterInfo = newSelection; setState(() { diff --git a/lib/school/index.dart b/lib/school/index.dart index c342dd290..de963f8ce 100644 --- a/lib/school/index.dart +++ b/lib/school/index.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/login_status.dart'; import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/school/class2nd/index.dart'; import 'package:sit/school/event.dart'; import 'package:sit/school/exam_arrange/index.dart'; @@ -15,33 +16,19 @@ import 'package:sit/school/ywb/index.dart'; import 'package:rettulf/rettulf.dart'; import 'i18n.dart'; -class SchoolPage extends StatefulWidget { +class SchoolPage extends ConsumerStatefulWidget { const SchoolPage({super.key}); @override - State createState() => _SchoolPageState(); + ConsumerState createState() => _SchoolPageState(); } -class _SchoolPageState extends State { - LoginStatus? loginStatus; - OaUserType? userType; - - @override - void didChangeDependencies() { - final auth = context.auth; - final newLoginStatus = auth.loginStatus; - final newUserType = auth.userType; - if (loginStatus != newLoginStatus || userType != newUserType) { - setState(() { - loginStatus = newLoginStatus; - userType = newUserType; - }); - } - super.didChangeDependencies(); - } +class _SchoolPageState extends ConsumerState { @override Widget build(BuildContext context) { + final userType = ref.watch(CredentialsInit.storage.$oaUserType); + final loginStatus = ref.watch(CredentialsInit.storage.$oaLoginStatus); return Scaffold( resizeToAvoidBottomInset: false, body: NestedScrollView( diff --git a/lib/school/oa_announce/page/list.dart b/lib/school/oa_announce/page/list.dart index a3348de33..0245429cb 100644 --- a/lib/school/oa_announce/page/list.dart +++ b/lib/school/oa_announce/page/list.dart @@ -2,7 +2,8 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/common.dart'; @@ -15,17 +16,18 @@ import '../entity/announce.dart'; import '../init.dart'; import '../i18n.dart'; -class OaAnnounceListPage extends StatefulWidget { +class OaAnnounceListPage extends ConsumerStatefulWidget { const OaAnnounceListPage({super.key}); @override - State createState() => _OaAnnounceListPageState(); + ConsumerState createState() => _OaAnnounceListPageState(); } -class _OaAnnounceListPageState extends State { +class _OaAnnounceListPageState extends ConsumerState { @override Widget build(BuildContext context) { - final cats = OaAnnounceCat.resolve(context.auth.userType); + final userType = ref.watch(CredentialsInit.storage.$oaUserType); + final cats = OaAnnounceCat.resolve(userType); return OaAnnounceListPageInternal(cats: cats); } } diff --git a/lib/settings/page/credentials.dart b/lib/settings/page/credentials.dart index 4d0c91cdf..73a28feaf 100644 --- a/lib/settings/page/credentials.dart +++ b/lib/settings/page/credentials.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/init.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/editor.dart'; import 'package:rettulf/rettulf.dart'; @@ -14,14 +14,14 @@ import '../i18n.dart'; const _changePasswordUrl = "https://authserver.sit.edu.cn/authserver/passwordChange.do"; -class CredentialsPage extends StatefulWidget { +class CredentialsPage extends ConsumerStatefulWidget { const CredentialsPage({super.key}); @override - State createState() => _CredentialsPageState(); + ConsumerState createState() => _CredentialsPageState(); } -class _CredentialsPageState extends State { +class _CredentialsPageState extends ConsumerState { var showPassword = false; @override @@ -43,7 +43,7 @@ class _CredentialsPageState extends State { } Widget buildBody() { - final credentials = context.auth.credentials; + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); final all = []; if (credentials != null) { all.add((_) => buildAccount(credentials)); diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index a43077b1b..cf2f95601 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/login/i18n.dart'; @@ -52,17 +52,17 @@ class _SettingsPageState extends ConsumerState { } List buildEntries() { + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); + final loginStatus = ref.watch(CredentialsInit.storage.$oaLoginStatus); final devOn = ref.watch(Dev.$on) ?? false; final all = []; - final auth = context.auth; - if (auth.loginStatus != LoginStatus.never) { + if (loginStatus != LoginStatus.never) { all.add(const CampusSelector().padSymmetric(h: 8)); } - final credential = auth.credentials; - if (credential != null) { + if (credentials != null) { all.add(PageNavigationTile( title: i18n.oaCredentials.oaAccount.text(), - subtitle: credential.account.text(), + subtitle: credentials.account.text(), leading: const Icon(Icons.person_rounded), path: "/settings/credentials", )); @@ -93,7 +93,7 @@ class _SettingsPageState extends ConsumerState { )); all.add(const Divider()); - if (auth.loginStatus != LoginStatus.never) { + if (loginStatus != LoginStatus.never) { all.add(PageNavigationTile( leading: const Icon(Icons.calendar_month_outlined), title: i18n.timetable.title.text(), @@ -129,7 +129,7 @@ class _SettingsPageState extends ConsumerState { )); all.add(const NetworkToolEntryTile()); } - if (auth.loginStatus != LoginStatus.never) { + if (loginStatus != LoginStatus.never) { all.add(const ClearCacheTile()); } all.add(const WipeDataTile()); diff --git a/lib/settings/page/school.dart b/lib/settings/page/school.dart index ec95f95ae..6a6a20a29 100644 --- a/lib/settings/page/school.dart +++ b/lib/settings/page/school.dart @@ -1,37 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/settings/settings.dart'; import 'package:rettulf/rettulf.dart'; import '../i18n.dart'; -class SchoolSettingsPage extends StatefulWidget { +class SchoolSettingsPage extends ConsumerStatefulWidget { const SchoolSettingsPage({ super.key, }); @override - State createState() => _SchoolSettingsPageState(); + ConsumerState createState() => _SchoolSettingsPageState(); } -class _SchoolSettingsPageState extends State { - OaUserType? userType; - - @override - void didChangeDependencies() { - final auth = context.auth; - final newUserType = auth.userType; - if (userType != newUserType) { - setState(() { - userType = newUserType; - }); - } - super.didChangeDependencies(); - } +class _SchoolSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { + final userType = ref.watch(CredentialsInit.storage.$oaUserType); return Scaffold( body: CustomScrollView( physics: const RangeMaintainingScrollPhysics(), diff --git a/lib/timetable/page/import.dart b/lib/timetable/page/import.dart index 1ee310e6b..4c14baa01 100644 --- a/lib/timetable/page/import.dart +++ b/lib/timetable/page/import.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/animation/animated.dart'; import 'package:sit/network/checker.dart'; @@ -29,14 +29,14 @@ enum ImportStatus { failed; } -class ImportTimetablePage extends StatefulWidget { +class ImportTimetablePage extends ConsumerStatefulWidget { const ImportTimetablePage({super.key}); @override - State createState() => _ImportTimetablePageState(); + ConsumerState createState() => _ImportTimetablePageState(); } -class _ImportTimetablePageState extends State { +class _ImportTimetablePageState extends ConsumerState { bool canImport = false; var _status = ImportStatus.none; late SemesterInfo initial = estimateCurrentSemester(); @@ -109,10 +109,11 @@ class _ImportTimetablePageState extends State { } Widget buildImportPage({Key? key}) { + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); return [ buildTip(context).padSymmetric(v: 30), SemesterSelector( - baseYear: getAdmissionYearFromStudentId(context.auth.credentials?.account), + baseYear: getAdmissionYearFromStudentId(credentials?.account), initial: initial, onSelected: (newSelection) { setState(() { @@ -131,7 +132,8 @@ class _ImportTimetablePageState extends State { final SemesterInfo(:exactYear, :semester) = info; final defaultName = i18n.import.defaultName(semester.l10n(), exactYear.toString(), (exactYear + 1).toString()); DateTime defaultStartDate = estimateStartDate(exactYear, semester); - if (context.auth.userType == OaUserType.undergraduate) { + final userType = ref.read(CredentialsInit.storage.$oaUserType); + if (userType == OaUserType.undergraduate) { final current = estimateCurrentSemester(); if (info == current) { final span = await TimetableInit.service.getUgSemesterSpan(); @@ -176,10 +178,12 @@ class _ImportTimetablePageState extends State { } Future getTimetable(SemesterInfo info) async { - return switch (context.auth.userType) { + final userType = ref.read(CredentialsInit.storage.$oaUserType); + return switch (userType) { OaUserType.undergraduate => TimetableInit.service.fetchUgTimetable(info), OaUserType.postgraduate => TimetableInit.service.fetchPgTimetable(info), OaUserType.other => throw Exception("Timetable importing not supported"), + _ => throw Exception("Timetable importing not supported"), }; } diff --git a/lib/utils/hive.dart b/lib/utils/hive.dart index 0b37600ad..74d88ce72 100644 --- a/lib/utils/hive.dart +++ b/lib/utils/hive.dart @@ -43,6 +43,27 @@ class BoxFieldNotifier extends StateNotifier { } } +class BoxFieldWithDefaultNotifier extends StateNotifier { + final Listenable listenable; + final T? Function() get; + final T Function() getDefault; + final FutureOr Function(T? v) set; + + BoxFieldWithDefaultNotifier(super._state, this.listenable, this.get, this.set, this.getDefault) { + listenable.addListener(_refresh); + } + + void _refresh() { + state = get() ?? getDefault(); + } + + @override + void dispose() { + listenable.removeListener(_refresh); + super.dispose(); + } +} + class BoxChangeNotifier extends ChangeNotifier { final Listenable listenable; @@ -123,6 +144,24 @@ extension BoxProviderX on Box { }); } + /// For generic class, like [List] or [Map], please specify the [get] for type conversion. + AutoDisposeStateNotifierProvider, T> providerWithDefault( + dynamic key, + T Function() getDefault, { + T? Function()? get, + FutureOr Function(T? v)? set, + }) { + return StateNotifierProvider.autoDispose, T>((ref) { + return BoxFieldWithDefaultNotifier( + get?.call() ?? safeGet(key) ?? getDefault(), + listenable(keys: [key]), + () => get?.call() ?? safeGet(key), + (v) => set?.call(v) ?? safePut(key, v), + getDefault, + ); + }); + } + /// For generic class, like [List] or [Map], please specify the [get] for type conversion. AutoDisposeStateNotifierProviderFamily, T?, Arg> providerFamily( dynamic Function(Arg arg) keyOf, { From b6a5da2d34f5599cc176d518626150fdce16f03d Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 05:27:57 +0800 Subject: [PATCH 054/458] [expense] user override on dev mode --- lib/app.dart | 2 +- lib/design/adaptive/editor.dart | 10 ++--- lib/life/expense_records/aggregated.dart | 10 ++++- lib/life/expense_records/index.dart | 2 +- lib/life/expense_records/page/records.dart | 2 +- .../expense_records/widget/chart/pie.dart | 2 +- lib/settings/dev.dart | 15 +++++-- lib/settings/page/about.dart | 4 +- lib/settings/page/developer.dart | 40 ++++++++++++++++--- lib/settings/page/index.dart | 2 +- 10 files changed, 65 insertions(+), 24 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index c1b3451f2..0d319baec 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -57,7 +57,7 @@ class _MimirAppState extends ConsumerState { @override Widget build(BuildContext context) { - final demoMode = ref.watch(Dev.$demoMode) ?? false; + final demoMode = ref.watch(Dev.$demoMode); final themeColorFromSystem = ref.watch(Settings.theme.$themeColorFromSystem) ?? true; ref.listen(Settings.$focusTimetable, (pre, next) { $routingConfig.value = next ?? false ? buildTimetableFocusRouter() : buildCommonRoutingConfig(); diff --git a/lib/design/adaptive/editor.dart b/lib/design/adaptive/editor.dart index 8e6f9c826..6e89fca03 100644 --- a/lib/design/adaptive/editor.dart +++ b/lib/design/adaptive/editor.dart @@ -200,7 +200,7 @@ class _EnumEditorState extends State> { secondary: $Action$( text: _i18n.cancel, onPressed: () { - context.navigator.pop(widget.initial); + context.navigator.pop(); }, ), make: (ctx) => PlatformTextButton( @@ -260,7 +260,7 @@ class _DateTimeEditorState extends State { secondary: $Action$( text: _i18n.cancel, onPressed: () { - context.navigator.pop(widget.initial); + context.navigator.pop(); }), make: (ctx) => PlatformTextButton( child: current.toString().text(), @@ -321,7 +321,7 @@ class _IntEditorState extends State { secondary: $Action$( text: _i18n.cancel, onPressed: () { - context.navigator.pop(widget.initial); + context.navigator.pop(); }), make: (ctx) => buildBody(ctx)); } @@ -392,7 +392,7 @@ class _BoolEditorState extends State { secondary: $Action$( text: _i18n.cancel, onPressed: () { - context.navigator.pop(widget.initial); + context.navigator.pop(); }), make: (ctx) => $ListTile$( title: (widget.desc ?? "").text(), @@ -446,7 +446,7 @@ class _StringEditorState extends State { secondary: $Action$( text: _i18n.cancel, onPressed: () { - context.navigator.pop(widget.initial); + context.navigator.pop(); }), make: (ctx) => $TextField$( maxLines: lines, diff --git a/lib/life/expense_records/aggregated.dart b/lib/life/expense_records/aggregated.dart index 7c057f4ce..c85aa6ddd 100644 --- a/lib/life/expense_records/aggregated.dart +++ b/lib/life/expense_records/aggregated.dart @@ -1,15 +1,21 @@ +import 'package:sit/settings/dev.dart'; + import 'entity/local.dart'; import 'init.dart'; class ExpenseAggregated { static Future fetchAndSaveTransactionUntilNow({ - required String studentId, + required String oaAccount, }) async { + final override = Dev.expenseUserOverride; + if (override != null) { + oaAccount = override; + } final storage = ExpenseRecordsInit.storage; final now = DateTime.now(); final start = now.copyWith(year: now.year - 4); final newlyFetched = await ExpenseRecordsInit.service.fetch( - studentID: studentId, + studentID: oaAccount, from: start, to: now, ); diff --git a/lib/life/expense_records/index.dart b/lib/life/expense_records/index.dart index 7023442ce..d45c7f970 100644 --- a/lib/life/expense_records/index.dart +++ b/lib/life/expense_records/index.dart @@ -59,7 +59,7 @@ class _ExpenseRecordsAppCardState extends ConsumerState { if (credentials == null) return; try { await ExpenseAggregated.fetchAndSaveTransactionUntilNow( - studentId: credentials.account, + oaAccount: credentials.account, ); } catch (error) { if (active) { diff --git a/lib/life/expense_records/page/records.dart b/lib/life/expense_records/page/records.dart index cbd627421..10b8751d8 100644 --- a/lib/life/expense_records/page/records.dart +++ b/lib/life/expense_records/page/records.dart @@ -52,7 +52,7 @@ class _ExpenseRecordsPageState extends ConsumerState { }); try { await ExpenseAggregated.fetchAndSaveTransactionUntilNow( - studentId: credentials.account, + oaAccount: credentials.account, ); updateRecords(ExpenseRecordsInit.storage.getTransactionsByRange()); setState(() { diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index 6724b19aa..277e5e365 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -114,7 +114,7 @@ class ExpensePieChartHeader extends StatelessWidget { return ExpenseChartHeader( upper: "Average expenses", content: "¥${average.toStringAsFixed(2)}", - lower: "for ${type.l10n()}", + lower: "in ${type.l10n()}", ); } } diff --git a/lib/settings/dev.dart b/lib/settings/dev.dart index 4f1a215d3..d4f90b2ea 100644 --- a/lib/settings/dev.dart +++ b/lib/settings/dev.dart @@ -8,6 +8,7 @@ class _K { static const on = '$ns/on'; static const savedOaCredentialsList = '$ns/savedOaCredentialsList'; static const demoMode = '$ns/demoMode'; + static const expenseUserOverride = '$ns/expenseUserOverride'; } // ignore: non_constant_identifier_names @@ -19,18 +20,24 @@ class DevSettingsImpl { DevSettingsImpl(this.box); /// [false] by default. - bool get on => box.safeGet(_K.on) ?? false; + bool get on => box.safeGet(_K.on) ?? false; set on(bool newV) => box.safePut(_K.on, newV); - late final $on = box.provider(_K.on); + late final $on = box.providerWithDefault(_K.on, () => false); /// [false] by default. - bool get demoMode => box.safeGet(_K.demoMode) ?? false; + bool get demoMode => box.safeGet(_K.demoMode) ?? false; set demoMode(bool newV) => box.safePut(_K.demoMode, newV); - late final $demoMode = box.provider(_K.demoMode); + late final $demoMode = box.providerWithDefault(_K.demoMode, () => false); + + String? get expenseUserOverride => box.safeGet(_K.expenseUserOverride); + + set expenseUserOverride(String? newV) => box.safePut(_K.expenseUserOverride, newV); + + late final $expenseUserOverride = box.provider(_K.expenseUserOverride); List? getSavedOaCredentialsList() => (box.safeGet(_K.savedOaCredentialsList) as List?)?.cast(); diff --git a/lib/settings/page/about.dart b/lib/settings/page/about.dart index d4bfbda87..9f26a28a4 100644 --- a/lib/settings/page/about.dart +++ b/lib/settings/page/about.dart @@ -103,7 +103,7 @@ class _VersionTileState extends ConsumerState { @override Widget build(BuildContext context) { - final devOn = ref.watch(Dev.$on) ?? false; + final devOn = ref.watch(Dev.$on); final version = R.currentVersion; return ListTile( leading: switch (version.platform) { @@ -120,7 +120,7 @@ class _VersionTileState extends ConsumerState { onTap: devOn && clickCount <= 10 ? null : () async { - if (ref.read(Dev.$on) ?? false) return; + if (ref.read(Dev.$on)) return; clickCount++; if (clickCount >= 10) { clickCount = 0; diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index 31e5343a7..0a9730cf3 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -1,11 +1,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/credentials/utils.dart'; +import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/editor.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/expansion_tile.dart'; @@ -51,7 +53,7 @@ class _DeveloperOptionsPageState extends ConsumerState { path: "/settings/developer/local-storage", ), buildReload(), - const DebugExpenseUserTile(), + const DebugExpenseUserOverrideTile(), if (oaCredentials != null) SwitchOaUserTile( currentCredentials: oaCredentials, @@ -65,7 +67,7 @@ class _DeveloperOptionsPageState extends ConsumerState { } Widget buildDevModeToggle() { - final on = ref.watch(Dev.$on) ?? false; + final on = ref.watch(Dev.$on); return ListTile( title: i18n.dev.devMode.text(), leading: const Icon(Icons.developer_mode_outlined), @@ -79,7 +81,7 @@ class _DeveloperOptionsPageState extends ConsumerState { } Widget buildDemoModeToggle() { - final demoMode = ref.watch(Dev.$demoMode) ?? false; + final demoMode = ref.watch(Dev.$demoMode); return ListTile( title: i18n.dev.demoMode.text(), trailing: Switch.adaptive( @@ -202,6 +204,10 @@ class _SwitchOaUserTileState extends State { onTap: () async { await loginWith(credentials); }, + onLongPress: () async { + context.showSnackBar(content: i18n.copyTipOf(i18n.oaCredentials.oaAccount).text()); + await Clipboard.setData(ClipboardData(text: credentials.account)); + }, ).padH(12); } @@ -239,13 +245,35 @@ class _SwitchOaUserTileState extends State { } } -class DebugExpenseUserTile extends StatelessWidget { - const DebugExpenseUserTile({super.key}); +class DebugExpenseUserOverrideTile extends ConsumerWidget { + const DebugExpenseUserOverrideTile({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(Dev.$expenseUserOverride); return ListTile( + leading: Icon(context.icons.person), title: "Expense user".text(), + subtitle: user?.text(), + onTap: () async { + final res = await Editor.showStringEditor( + context, + desc: "OA account", + initial: user ?? "", + ); + if (res == null || res.isEmpty) return; + if (estimateOaUserType(res) == null) { + if (!context.mounted) return; + await context.showTip( + title: "Error", + desc: "Invalid OA account format.", + ok: "OK", + ); + } else { + ref.read(Dev.$expenseUserOverride.notifier).set(res); + } + }, + trailing: Icon(context.icons.edit), ); } } diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index cf2f95601..31bfd0f00 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -54,7 +54,7 @@ class _SettingsPageState extends ConsumerState { List buildEntries() { final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); final loginStatus = ref.watch(CredentialsInit.storage.$oaLoginStatus); - final devOn = ref.watch(Dev.$on) ?? false; + final devOn = ref.watch(Dev.$on); final all = []; if (loginStatus != LoginStatus.never) { all.add(const CampusSelector().padSymmetric(h: 8)); From da20cc8141d388b8e8ac3b6c42544ef02c51528b Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 05:34:25 +0800 Subject: [PATCH 055/458] [expense] [statistics] list view inside list view --- lib/life/expense_records/page/statistics.dart | 15 +++++++++------ lib/life/expense_records/widget/chart/pie.dart | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 3d333ad8f..1cdc99ad9 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,7 +1,4 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; @@ -113,9 +110,15 @@ class _ExpenseStatisticsPageState extends ConsumerState { const Divider(), ExpensePieChart(records: current.records), const Divider(), - ...current.records.map((record) { - return TransactionTile(record); - }), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: current.records.length, + itemBuilder: (ctx, i) { + final record = current.records[i]; + return TransactionTile(record); + }, + ), ], ), // ListView() diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index 277e5e365..fe6e6c99a 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -27,7 +27,7 @@ class _ExpensePieChartState extends State { @override Widget build(BuildContext context) { assert(widget.records.every((type) => type.isConsume)); - final (:total, :parts) = separateTransactionByType(widget.records); + final (total: _, :parts) = separateTransactionByType(widget.records); final ascending = parts.entries.sortedBy((e) => e.value.total); final atMost = ascending.last; return [ From ef85cc143e6477ee2f8533d1adfbeed6d3512663 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 12:47:25 +0800 Subject: [PATCH 056/458] [dev] clear expense user override --- lib/life/expense_records/aggregated.dart | 2 +- lib/settings/page/developer.dart | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/life/expense_records/aggregated.dart b/lib/life/expense_records/aggregated.dart index c85aa6ddd..014032236 100644 --- a/lib/life/expense_records/aggregated.dart +++ b/lib/life/expense_records/aggregated.dart @@ -13,7 +13,7 @@ class ExpenseAggregated { } final storage = ExpenseRecordsInit.storage; final now = DateTime.now(); - final start = now.copyWith(year: now.year - 4); + final start = now.copyWith(year: now.year - 6); final newlyFetched = await ExpenseRecordsInit.service.fetch( studentID: oaAccount, from: start, diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index 0a9730cf3..b998b4874 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -141,12 +141,13 @@ class _DebugGoRouteTileState extends State { ), trailing: [ $route >> - (ctx, route) => PlatformIconButton( + (ctx, route) => + PlatformIconButton( onPressed: route.text.isEmpty ? null : () { - context.push(route.text); - }, + context.push(route.text); + }, icon: const Icon(Icons.arrow_forward), ) ].row(mas: MainAxisSize.min), @@ -182,9 +183,9 @@ class _SwitchOaUserTileState extends State { leading: const Icon(Icons.swap_horiz), trailing: isLoggingIn ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator.adaptive(), - ) + padding: EdgeInsets.all(8), + child: CircularProgressIndicator.adaptive(), + ) : null, children: [ ...credentialsList.map(buildCredentialsHistoryTile), @@ -261,7 +262,11 @@ class DebugExpenseUserOverrideTile extends ConsumerWidget { desc: "OA account", initial: user ?? "", ); - if (res == null || res.isEmpty) return; + if (res == null) return; + if (res.isEmpty) { + ref.read(Dev.$expenseUserOverride.notifier).set(null); + return; + } if (estimateOaUserType(res) == null) { if (!context.mounted) return; await context.showTip( From 31217e0e87076e0cf17593cb7f83d8ab18fa374b Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 19:37:55 +0800 Subject: [PATCH 057/458] [expense] [statistics] daily average and monthly average --- .../expense_records/widget/chart/bar.dart | 56 +++++++++---------- .../expense_records/widget/chart/header.dart | 6 +- .../expense_records/widget/chart/pie.dart | 23 ++++++-- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 79d4251e7..669a46246 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -42,11 +42,7 @@ class _ExpenseBarChartState extends State { records: widget.records, ); return [ - ExpenseBarChartHeader( - from: widget.start, - to: widget.mode.getAfterUnitTime(start: widget.start, endLimit: DateTime.now()), - total: delegate.total, - ).padFromLTRB(16, 8, 0, 8), + buildChartHeader(delegate).padFromLTRB(16, 8, 0, 8), AspectRatio( aspectRatio: 1.5, child: AmountChartWidget( @@ -55,6 +51,23 @@ class _ExpenseBarChartState extends State { ), ].column(caa: CrossAxisAlignment.start); } + + Widget buildChartHeader(StatisticsDelegate delegate) { + final from = widget.start; + final to = widget.mode.getAfterUnitTime(start: widget.start, endLimit: DateTime.now()); + return switch (widget.mode) { + StatisticsMode.year => ExpenseChartHeader( + upper: "Monthly average", + content: "¥${delegate.average.toStringAsFixed(2)}", + lower: formatDateSpan(from: from, to: to), + ), + StatisticsMode.week || StatisticsMode.month => ExpenseChartHeader( + upper: "Daily average", + content: "¥${delegate.average.toStringAsFixed(2)}", + lower: formatDateSpan(from: from, to: to), + ), + }; + } } class StatisticsDelegate { @@ -104,11 +117,13 @@ class StatisticsDelegate { data[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday].add(record); } final amounts = records.map((r) => r.deltaAmount).toList(); + final dayTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); return StatisticsDelegate( mode: StatisticsMode.week, data: data, side: _buildSideTitle, - average: amounts.isEmpty ? double.infinity : amounts.mean, + average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, total: amounts.sum, bottom: (ctx, value, mate) { final index = value.toInt(); @@ -137,11 +152,13 @@ class StatisticsDelegate { data[record.timestamp.day - 1].add(record); } final amounts = records.map((r) => r.deltaAmount).toList(); + final dayTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); final sep = data.length ~/ 5; return StatisticsDelegate( mode: StatisticsMode.week, data: data, - average: amounts.isEmpty ? double.infinity : amounts.mean, + average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, total: amounts.sum, side: _buildSideTitle, bottom: (ctx, value, meta) { @@ -172,10 +189,12 @@ class StatisticsDelegate { data[record.timestamp.month - 1].add(record); } final amounts = records.map((r) => r.deltaAmount).toList(); + final monthTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); return StatisticsDelegate( mode: StatisticsMode.week, data: data, - average: amounts.isEmpty ? double.infinity : amounts.mean, + average: monthTotals.isEmpty ? 0.0 : monthTotals.mean, total: amounts.sum, side: _buildSideTitle, bottom: (ctx, value, mate) { @@ -297,24 +316,3 @@ class AmountChartWidget extends StatelessWidget { } } -class ExpenseBarChartHeader extends StatelessWidget { - final double total; - final DateTime from; - final DateTime to; - - const ExpenseBarChartHeader({ - super.key, - required this.total, - required this.from, - required this.to, - }); - - @override - Widget build(BuildContext context) { - return ExpenseChartHeader( - upper: "Total", - content: "¥${total.toStringAsFixed(2)}", - lower: formatDateSpan(from: from, to: to), - ); - } -} diff --git a/lib/life/expense_records/widget/chart/header.dart b/lib/life/expense_records/widget/chart/header.dart index df91e1542..630447fab 100644 --- a/lib/life/expense_records/widget/chart/header.dart +++ b/lib/life/expense_records/widget/chart/header.dart @@ -4,13 +4,13 @@ import 'package:rettulf/rettulf.dart'; class ExpenseChartHeader extends StatelessWidget { final String upper; final String content; - final String lower; + final String? lower; const ExpenseChartHeader({ super.key, required this.upper, required this.content, - required this.lower, + this.lower, }); @override @@ -19,7 +19,7 @@ class ExpenseChartHeader extends StatelessWidget { return [ upper.text(style: labelStyle), content.text(style: context.textTheme.titleLarge), - lower.text(style: labelStyle), + if(lower != null) lower!.text(style: labelStyle), ].column(caa: CrossAxisAlignment.start); } } diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index fe6e6c99a..d07119b8b 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -27,13 +27,12 @@ class _ExpensePieChartState extends State { @override Widget build(BuildContext context) { assert(widget.records.every((type) => type.isConsume)); - final (total: _, :parts) = separateTransactionByType(widget.records); + final (total:total, :parts) = separateTransactionByType(widget.records); final ascending = parts.entries.sortedBy((e) => e.value.total); final atMost = ascending.last; return [ ExpensePieChartHeader( - average: atMost.value.total / atMost.value.records.length, - type: atMost.key, + total: total, ).padFromLTRB(16, 8, 0, 0), AspectRatio( aspectRatio: 1.5, @@ -100,10 +99,26 @@ class _ExpensePieChartState extends State { } class ExpensePieChartHeader extends StatelessWidget { + final double total; + + const ExpensePieChartHeader({ + super.key, + required this.total, + }); + + @override + Widget build(BuildContext context) { + return ExpenseChartHeader( + upper: "Total", + content: "¥${total.toStringAsFixed(2)}", + ); + } +} +class ExpenseChart2Header extends StatelessWidget { final TransactionType type; final double average; - const ExpensePieChartHeader({ + const ExpenseChart2Header({ super.key, required this.type, required this.average, From 1d35e604523834042e8d728c8459489b569244e5 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 19:51:09 +0800 Subject: [PATCH 058/458] [expense] [statistics] Weekday in calendar order --- lib/life/expense_records/widget/chart/bar.dart | 4 ++-- lib/utils/date.dart | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 669a46246..1559b7a8b 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -108,13 +108,13 @@ class StatisticsDelegate { }) { final now = DateTime.now(); final data = List.generate( - start.year == now.year && start.week == now.week ? now.weekday : 7, + start.year == now.year && start.week == now.week ? now.calendarOrderWeekday + 1 : 7, (i) => [], ); for (final record in records) { // add data at the same weekday. // sunday goes first - data[record.timestamp.weekday == DateTime.sunday ? 0 : record.timestamp.weekday].add(record); + data[record.timestamp.calendarOrderWeekday].add(record); } final amounts = records.map((r) => r.deltaAmount).toList(); final dayTotals = diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 28fce4977..81e6908df 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -62,6 +62,11 @@ int getWeek({ extension DateTimeX on DateTime { int get week => getWeek(year: year, month: month, day: day); + + int get calendarOrderWeekday { + final w = weekday; + return w == DateTime.sunday ? 0 : w; + } } DateTime getDateOfFirstDayInWeek({ From 778c6a92dac8aa5b4e792d8cea2e0866c4df1c1e Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 10 Apr 2024 20:06:36 +0800 Subject: [PATCH 059/458] [expense] [statistics] fixed bar chart for tiny value --- lib/life/expense_records/widget/chart/bar.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 1559b7a8b..315eca279 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -8,6 +8,7 @@ import 'package:rettulf/rettulf.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/route.dart'; import 'package:sit/utils/date.dart'; +import 'package:sit/utils/format.dart'; import 'package:statistics/statistics.dart'; import '../../entity/local.dart'; @@ -212,7 +213,7 @@ class StatisticsDelegate { } static Widget _buildSideTitle(BuildContext ctx, double value, TitleMeta meta) { - String text = '¥${value.round()}'; + String text = '¥${formatWithoutTrailingZeros(value)}'; return SideTitleWidget( axisSide: meta.axisSide, child: Text( @@ -249,7 +250,7 @@ class AmountChartWidget extends StatelessWidget { leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 50, + reservedSize: 60, getTitlesWidget: (v, meta) => delegate.side(context, v, meta), ), ), From 4dc260b73d54b9479f85ec62a7daaa32acbcb3cb Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 01:37:59 +0800 Subject: [PATCH 060/458] [expense] [statistics] pie chart summary --- .../expense_records/widget/chart/bar.dart | 2 +- .../expense_records/widget/chart/header.dart | 20 ++++++++++-- .../expense_records/widget/chart/pie.dart | 31 +++++++++++++------ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 315eca279..1382f9bac 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -269,7 +269,7 @@ class AmountChartWidget extends StatelessWidget { show: true, alignment: Alignment.bottomRight, style: context.textTheme.labelSmall, - labelResolver: (line) => "¥${line.y.toStringAsFixed(2)}", + labelResolver: (line) => "avg", ), y: delegate.average, strokeWidth: 3, diff --git a/lib/life/expense_records/widget/chart/header.dart b/lib/life/expense_records/widget/chart/header.dart index 630447fab..97e9c7afd 100644 --- a/lib/life/expense_records/widget/chart/header.dart +++ b/lib/life/expense_records/widget/chart/header.dart @@ -15,11 +15,25 @@ class ExpenseChartHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final labelStyle = context.textTheme.titleMedium?.copyWith(color: context.theme.disabledColor); return [ - upper.text(style: labelStyle), + ExpenseChartHeaderLabel(upper), content.text(style: context.textTheme.titleLarge), - if(lower != null) lower!.text(style: labelStyle), + if (lower != null) ExpenseChartHeaderLabel(lower!), ].column(caa: CrossAxisAlignment.start); } } + +class ExpenseChartHeaderLabel extends StatelessWidget { + final String text; + + const ExpenseChartHeaderLabel( + this.text, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final style = context.textTheme.titleMedium?.copyWith(color: context.theme.disabledColor); + return text.text(style: style); + } +} diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index d07119b8b..b171a83ba 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -3,6 +3,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:statistics/statistics.dart'; import '../../entity/local.dart'; import "../../i18n.dart"; @@ -27,9 +28,8 @@ class _ExpensePieChartState extends State { @override Widget build(BuildContext context) { assert(widget.records.every((type) => type.isConsume)); - final (total:total, :parts) = separateTransactionByType(widget.records); - final ascending = parts.entries.sortedBy((e) => e.value.total); - final atMost = ascending.last; + final (total: total, :parts) = separateTransactionByType(widget.records); + final ascending = parts.entries.sortedBy((e) => e.value.total).reversed.toList(); return [ ExpensePieChartHeader( total: total, @@ -39,6 +39,16 @@ class _ExpensePieChartState extends State { child: buildChart(parts), ), buildLegends(parts).padAll(8).align(at: Alignment.topLeft), + const Divider(), + ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), + ...ascending.map((e) { + final amounts = e.value.records.map((e) => e.deltaAmount).toList(); + return ExpenseAverageTile( + average: amounts.mean, + max: amounts.max, + type: e.key, + ); + }), ].column(caa: CrossAxisAlignment.start); } @@ -114,22 +124,25 @@ class ExpensePieChartHeader extends StatelessWidget { ); } } -class ExpenseChart2Header extends StatelessWidget { + +class ExpenseAverageTile extends StatelessWidget { final TransactionType type; final double average; + final double max; - const ExpenseChart2Header({ + const ExpenseAverageTile({ super.key, required this.type, required this.average, + required this.max, }); @override Widget build(BuildContext context) { - return ExpenseChartHeader( - upper: "Average expenses", - content: "¥${average.toStringAsFixed(2)}", - lower: "in ${type.l10n()}", + return ListTile( + leading: Icon(type.icon, color: type.color), + title: "You average spent ¥${average.toStringAsFixed(2)} in ${type.l10n()}".text(), + subtitle: "With a max spend of ¥${max.toStringAsFixed(2)}".text(), ); } } From 00c95e16c71f668cf2d3cb86db7fc6979c89c601 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 01:45:39 +0800 Subject: [PATCH 061/458] Card.filled and Card.outlined --- lib/design/widgets/app.dart | 6 +- lib/design/widgets/card.dart | 93 ++----------------- lib/design/widgets/grouped.dart | 5 +- lib/life/expense_records/page/statistics.dart | 3 +- lib/school/class2nd/page/attended.dart | 5 +- lib/school/class2nd/widgets/activity.dart | 5 +- lib/school/exam_arrange/page/list.dart | 3 +- lib/school/library/page/history.dart | 5 +- lib/school/library/page/search_result.dart | 5 +- lib/school/oa_announce/page/list.dart | 5 +- lib/settings/page/theme_color.dart | 7 +- lib/timetable/page/p13n/background.dart | 5 +- lib/timetable/page/p13n/palette.dart | 5 +- lib/timetable/page/p13n/palette_editor.dart | 7 +- lib/timetable/widgets/course.dart | 5 +- lib/timetable/widgets/timetable/daily.dart | 9 +- lib/timetable/widgets/timetable/weekly.dart | 7 +- 17 files changed, 45 insertions(+), 135 deletions(-) diff --git a/lib/design/widgets/app.dart b/lib/design/widgets/app.dart index e3560d3b5..ed9d3c818 100644 --- a/lib/design/widgets/app.dart +++ b/lib/design/widgets/app.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; -import 'card.dart'; - class AppCard extends StatelessWidget { /// [SizedBox] by default. final Widget? view; @@ -37,8 +35,8 @@ class AppCard extends StatelessWidget { final leftActions = this.leftActions ?? const []; final rightActions = this.rightActions ?? const []; final textTheme = context.textTheme; - return FilledCard( - clip: Clip.hardEdge, + return Card.filled( + clipBehavior: Clip.hardEdge, child: [ Theme( data: context.theme.copyWith( diff --git a/lib/design/widgets/card.dart b/lib/design/widgets/card.dart index 8b50acf07..05863e8bd 100644 --- a/lib/design/widgets/card.dart +++ b/lib/design/widgets/card.dart @@ -1,94 +1,21 @@ import 'package:flutter/material.dart'; -class OutlinedCard extends StatelessWidget { - final Widget? child; - final EdgeInsetsGeometry? margin; - final Clip? clip; - final Color? color; - - const OutlinedCard({ - super.key, - this.child, - this.margin, - this.color, - this.clip, - }); - - @override - Widget build(BuildContext context) { - return Card( - margin: margin, - clipBehavior: clip, - elevation: 0, - shape: RoundedRectangleBorder( - side: BorderSide( - color: color ?? Theme.of(context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - child: child, - ); - } -} - -class FilledCard extends StatelessWidget { - final Widget? child; - final EdgeInsetsGeometry? margin; - final Color? color; - final Clip? clip; - final ShapeBorder? shape; - - const FilledCard({ - super.key, - this.child, - this.margin, - this.color, - this.clip, - this.shape, - }); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 0, - shape: shape, - clipBehavior: clip, - color: color ?? Theme.of(context).colorScheme.surfaceVariant, - margin: margin, - child: child, - ); - } -} - extension WidgetCardX on Widget { Widget inOutlinedCard({ Clip? clip, }) { - return Builder( - builder: (context) => Card( - elevation: 0, - clipBehavior: clip, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - child: this, - ), + return Card.outlined( + clipBehavior: clip, + child: this, ); } Widget inFilledCard({ Clip? clip, }) { - return Builder( - builder: (context) => Card( - elevation: 0, - clipBehavior: clip, - color: Theme.of(context).colorScheme.surfaceVariant, - child: this, - ), + return Card.filled( + clipBehavior: clip, + child: this, ); } @@ -101,12 +28,12 @@ extension WidgetCardX on Widget { clipBehavior: clip, child: this, ), - CardType.filled => FilledCard( - clip: clip, + CardType.filled => Card.filled( + clipBehavior: clip, child: this, ), - CardType.outlined => OutlinedCard( - clip: clip, + CardType.outlined => Card.outlined( + clipBehavior: clip, child: this, ), }; diff --git a/lib/design/widgets/grouped.dart b/lib/design/widgets/grouped.dart index 2bfcf62e0..ddd954ef6 100644 --- a/lib/design/widgets/grouped.dart +++ b/lib/design/widgets/grouped.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sliver_tools/sliver_tools.dart'; typedef HeaderBuilder = Widget Function( @@ -117,8 +116,8 @@ class _AsyncGroupSectionState extends State> { pushPinnedChildren: true, children: [ SliverPinnedHeader( - child: FilledCard( - clip: Clip.hardEdge, + child: Card.filled( + clipBehavior: Clip.hardEdge, child: ListTile( title: widget.title, subtitle: widget.subtitle, diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 1cdc99ad9..67d314eca 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; import 'package:sit/life/expense_records/storage/local.dart'; import 'package:rettulf/rettulf.dart'; @@ -154,7 +153,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { Widget buildHeader(DateTime start) { final startTime2Records = ref.watch(_startTime2Records); final mode = ref.watch(_statisticsMode); - return FilledCard( + return Card.filled( child: [ PlatformIconButton( onPressed: index > 0 diff --git a/lib/school/class2nd/page/attended.dart b/lib/school/class2nd/page/attended.dart index 8c7af73a5..548f6a495 100644 --- a/lib/school/class2nd/page/attended.dart +++ b/lib/school/class2nd/page/attended.dart @@ -9,7 +9,6 @@ import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/animation/progress.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/widgets/list_tile.dart'; @@ -243,8 +242,8 @@ class AttendedActivityCard extends StatelessWidget { Widget build(BuildContext context) { final (:title, :tags) = separateTagsFromTitle(attended.title); final points = attended.calcTotalPoints(); - return FilledCard( - clip: Clip.hardEdge, + return Card.filled( + clipBehavior: Clip.hardEdge, child: ListTile( isThreeLine: true, title: title.text(), diff --git a/lib/school/class2nd/widgets/activity.dart b/lib/school/class2nd/widgets/activity.dart index feaf34baf..06ec79a85 100644 --- a/lib/school/class2nd/widgets/activity.dart +++ b/lib/school/class2nd/widgets/activity.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/tags.dart'; import 'package:sit/l10n/extension.dart'; import 'package:rettulf/rettulf.dart'; @@ -21,8 +20,8 @@ class ActivityCard extends StatelessWidget { Widget build(BuildContext context) { final textTheme = context.textTheme; final (:title, :tags) = separateTagsFromTitle(activity.title); - return FilledCard( - clip: Clip.hardEdge, + return Card.filled( + clipBehavior: Clip.hardEdge, child: ListTile( isThreeLine: true, title: title.text(), diff --git a/lib/school/exam_arrange/page/list.dart b/lib/school/exam_arrange/page/list.dart index 050497c11..41dde9668 100644 --- a/lib/school/exam_arrange/page/list.dart +++ b/lib/school/exam_arrange/page/list.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/credentials/init.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:sit/school/entity/school.dart'; import 'package:sit/school/utils.dart'; @@ -84,7 +83,7 @@ class _ExamArrangementListPageState extends ConsumerState with Auto SliverList.builder( itemCount: announcements.length, itemBuilder: (ctx, index) { - return FilledCard( - clip: Clip.hardEdge, + return Card.filled( + clipBehavior: Clip.hardEdge, child: OaAnnounceTile(announcements[index]), ); }, diff --git a/lib/settings/page/theme_color.dart b/lib/settings/page/theme_color.dart index 066f2e5e6..91db566cf 100644 --- a/lib/settings/page/theme_color.dart +++ b/lib/settings/page/theme_color.dart @@ -2,7 +2,6 @@ import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/settings/settings.dart'; import 'package:sit/utils/color.dart'; @@ -102,9 +101,9 @@ class _ThemeColorPageState extends State { title: i18n.themeColor.text(), subtitle: "#${themeColor.hexAlpha}".text(), onTap: selectNewThemeColor, - trailing: FilledCard( + trailing: Card.filled( color: fromSystem ? context.theme.disabledColor : themeColor, - clip: Clip.hardEdge, + clipBehavior: Clip.hardEdge, child: const SizedBox( width: 32, height: 32, @@ -151,7 +150,7 @@ class ThemeColorPreview extends StatelessWidget { ), ), ), - FilledCard( + Card.filled( child: _PreviewTile( trailing: (v, f) => Switch.adaptive( value: v, diff --git a/lib/timetable/page/p13n/background.dart b/lib/timetable/page/p13n/background.dart index f742c9dc0..1d76e20bd 100644 --- a/lib/timetable/page/p13n/background.dart +++ b/lib/timetable/page/p13n/background.dart @@ -8,7 +8,6 @@ import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:sit/files.dart'; import 'package:sit/settings/dev.dart'; @@ -177,8 +176,8 @@ class _TimetableBackgroundEditorState extends State w } Widget buildImage() { - return OutlinedCard( - clip: Clip.hardEdge, + return Card.outlined( + clipBehavior: Clip.hardEdge, child: buildPreviewBoxContent(), ); } diff --git a/lib/timetable/page/p13n/palette.dart b/lib/timetable/page/p13n/palette.dart index 809634f12..67061ec1d 100644 --- a/lib/timetable/page/p13n/palette.dart +++ b/lib/timetable/page/p13n/palette.dart @@ -9,7 +9,6 @@ import 'package:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/entry_card.dart'; import 'package:sit/l10n/extension.dart'; import 'package:sit/qrcode/page/view.dart'; @@ -406,13 +405,13 @@ class PaletteColorsPreview extends StatelessWidget { return colors .map((c) { final color = c.byBrightness(brightness); - return OutlinedCard( + return Card.outlined( color: brightness == Brightness.light ? Colors.black : Colors.white, margin: EdgeInsets.zero, child: TweenAnimationBuilder( tween: ColorTween(begin: color, end: color), duration: const Duration(milliseconds: 300), - builder: (ctx, value, child) => FilledCard( + builder: (ctx, value, child) => Card.filled( margin: EdgeInsets.zero, color: value, child: const SizedBox( diff --git a/lib/timetable/page/p13n/palette_editor.dart b/lib/timetable/page/p13n/palette_editor.dart index b0169b1dd..719719b00 100644 --- a/lib/timetable/page/p13n/palette_editor.dart +++ b/lib/timetable/page/p13n/palette_editor.dart @@ -8,7 +8,6 @@ import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/adaptive/swipe.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/l10n/extension.dart'; import 'package:sit/timetable/page/preview.dart'; import 'package:sit/timetable/platte.dart'; @@ -299,12 +298,12 @@ class PaletteColorBar extends StatelessWidget { Widget build(BuildContext context) { final onEdit = this.onEdit; final textColor = color.resolveTextColorForReadability(); - return OutlinedCard( + return Card.outlined( color: textColor, margin: EdgeInsets.zero, - child: FilledCard( + child: Card.filled( color: color, - clip: Clip.hardEdge, + clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: InkWell( onTap: onEdit == null diff --git a/lib/timetable/widgets/course.dart b/lib/timetable/widgets/course.dart index d3274dad8..189c27295 100644 --- a/lib/timetable/widgets/course.dart +++ b/lib/timetable/widgets/course.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/design/widgets/expansion_tile.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/school/widgets/course.dart'; @@ -25,8 +24,8 @@ class TimetableCourseCard extends StatelessWidget { @override Widget build(BuildContext context) { - return FilledCard( - clip: Clip.hardEdge, + return Card.filled( + clipBehavior: Clip.hardEdge, color: color, child: AnimatedExpansionTile( leading: CourseIcon(courseName: courseName), diff --git a/lib/timetable/widgets/timetable/daily.dart b/lib/timetable/widgets/timetable/daily.dart index a16fd4ed4..d1097b48e 100644 --- a/lib/timetable/widgets/timetable/daily.dart +++ b/lib/timetable/widgets/timetable/daily.dart @@ -5,7 +5,6 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/school/utils.dart'; import 'package:sit/school/entity/timetable.dart'; @@ -294,10 +293,10 @@ class LessonCard extends StatelessWidget { @override Widget build(BuildContext context) { - return FilledCard( + return Card.filled( margin: const EdgeInsets.all(8), color: color, - clip: Clip.hardEdge, + clipBehavior: Clip.hardEdge, child: ListTile( leading: CourseIcon(courseName: course.courseName), onTap: () async { @@ -376,7 +375,7 @@ class LessonOverlapGroup extends StatelessWidget { } // [classTime] must be nonnull. // TODO: Color for class overlap. - return OutlinedCard( + return Card.outlined( child: [ ClassTimeCard( color: TimetableStyle.of(context).platte.colors[0].byTheme(context.theme), @@ -443,7 +442,7 @@ class ElevatedText extends StatelessWidget { @override Widget build(BuildContext context) { - return FilledCard( + return Card.filled( color: color, child: child.padAll(margin), ); diff --git a/lib/timetable/widgets/timetable/weekly.dart b/lib/timetable/widgets/timetable/weekly.dart index 8c37f4aeb..394de4d97 100644 --- a/lib/timetable/widgets/timetable/weekly.dart +++ b/lib/timetable/widgets/timetable/weekly.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/dash_decoration.dart'; -import 'package:sit/design/widgets/card.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/school/utils.dart'; import 'package:sit/timetable/platte.dart'; @@ -162,7 +161,7 @@ class _TimetableOneWeekCachedState extends State with Au grayOut: style.cellStyle.grayOutTakenLessons ? passed : false, ); if (inClassNow) { - // cell = OutlinedCard( + // cell = Card.outlined( // margin: const EdgeInsets.all(1), // child: cell.padAll(1), // ); @@ -430,8 +429,8 @@ class CourseCell extends StatelessWidget { teachers: teachers, // textColor: color.resolveTextColorForReadability(), ).center(); - return FilledCard( - clip: Clip.hardEdge, + return Card.filled( + clipBehavior: Clip.hardEdge, color: color, margin: EdgeInsets.all(0.5.w), child: innerBuilder != null ? innerBuilder(context, info) : info, From 9afe172dd43785d13dbe42092a1849483341c95e Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 01:51:11 +0800 Subject: [PATCH 062/458] CardVariant --- lib/design/widgets/card.dart | 20 ++++++++++---------- lib/school/exam_result/widgets/pg.dart | 2 +- lib/school/library/page/borrowing.dart | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/design/widgets/card.dart b/lib/design/widgets/card.dart index 05863e8bd..46d472bd9 100644 --- a/lib/design/widgets/card.dart +++ b/lib/design/widgets/card.dart @@ -1,5 +1,11 @@ import 'package:flutter/material.dart'; +enum CardVariant { + elevated, + filled, + outlined; +} + extension WidgetCardX on Widget { Widget inOutlinedCard({ Clip? clip, @@ -21,27 +27,21 @@ extension WidgetCardX on Widget { Widget inAnyCard({ Clip? clip, - CardType type = CardType.plain, + CardVariant type = CardVariant.elevated, }) { return switch (type) { - CardType.plain => Card( + CardVariant.elevated => Card( clipBehavior: clip, child: this, ), - CardType.filled => Card.filled( + CardVariant.filled => Card.filled( clipBehavior: clip, child: this, ), - CardType.outlined => Card.outlined( + CardVariant.outlined => Card.outlined( clipBehavior: clip, child: this, ), }; } } - -enum CardType { - plain, - filled, - outlined; -} diff --git a/lib/school/exam_result/widgets/pg.dart b/lib/school/exam_result/widgets/pg.dart index 6585a2560..54bc5feed 100644 --- a/lib/school/exam_result/widgets/pg.dart +++ b/lib/school/exam_result/widgets/pg.dart @@ -36,7 +36,7 @@ class ExamResultPgCard extends StatelessWidget { color: result.passed ? null : context.$red$, ), trailing: result.score.toString().text(), - ).inAnyCard(clip: Clip.hardEdge, type: elevated ? CardType.plain : CardType.filled); + ).inAnyCard(clip: Clip.hardEdge, type: elevated ? CardVariant.elevated : CardVariant.filled); } } diff --git a/lib/school/library/page/borrowing.dart b/lib/school/library/page/borrowing.dart index 69b9e462c..f2dce2d20 100644 --- a/lib/school/library/page/borrowing.dart +++ b/lib/school/library/page/borrowing.dart @@ -138,6 +138,6 @@ class BorrowedBookCard extends StatelessWidget { ), ); }, - ).inAnyCard(clip: Clip.hardEdge, type: elevated ? CardType.plain : CardType.filled); + ).inAnyCard(clip: Clip.hardEdge, type: elevated ? CardVariant.elevated : CardVariant.filled); } } From cf10395bed86c6d0967953bd9374a6f2ff9a763f Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 02:06:07 +0800 Subject: [PATCH 063/458] [expense] [statistics] animation --- lib/design/animation/animated.dart | 6 +++--- lib/life/expense_records/widget/chart/pie.dart | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/design/animation/animated.dart b/lib/design/animation/animated.dart index fc312fda1..bb9876de5 100644 --- a/lib/design/animation/animated.dart +++ b/lib/design/animation/animated.dart @@ -1,8 +1,8 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; extension AnimatedEx on Widget { Widget animatedSwitched({ - Duration duration = const Duration(milliseconds: 500), + Duration duration = Durations.medium2, Curve? switchInCurve, Curve? switchOutCurve, }) => @@ -14,7 +14,7 @@ extension AnimatedEx on Widget { ); Widget animatedSized({ - Duration duration = const Duration(milliseconds: 500), + Duration duration = Durations.medium2, Alignment align = Alignment.center, Curve? curve, }) => diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index b171a83ba..d5762edd8 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -3,6 +3,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/design/animation/animated.dart'; import 'package:statistics/statistics.dart'; import '../../entity/local.dart'; @@ -40,7 +41,8 @@ class _ExpensePieChartState extends State { ), buildLegends(parts).padAll(8).align(at: Alignment.topLeft), const Divider(), - ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), + [ + ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), ...ascending.map((e) { final amounts = e.value.records.map((e) => e.deltaAmount).toList(); return ExpenseAverageTile( @@ -48,7 +50,7 @@ class _ExpensePieChartState extends State { max: amounts.max, type: e.key, ); - }), + })].column(caa: CrossAxisAlignment.start).animatedSized(), ].column(caa: CrossAxisAlignment.start); } @@ -141,8 +143,8 @@ class ExpenseAverageTile extends StatelessWidget { Widget build(BuildContext context) { return ListTile( leading: Icon(type.icon, color: type.color), - title: "You average spent ¥${average.toStringAsFixed(2)} in ${type.l10n()}".text(), - subtitle: "With a max spend of ¥${max.toStringAsFixed(2)}".text(), + title: "Average spent ¥${average.toStringAsFixed(2)} in ${type.l10n()}".text(), + subtitle: "with a max spend of ¥${max.toStringAsFixed(2)}".text(), ); } } From 5ef3a3ee2204c90ae4a687cfb475d91eb9e438c2 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 02:10:06 +0800 Subject: [PATCH 064/458] CHANGELOG.md --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ devtools_options.yaml | 1 + 2 files changed, 44 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 devtools_options.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..51086941a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changes + +## v2.3.0+25 +- The 2048 game supports save/load feature. +- You can add, delete, and edit courses in the timetable. +- The Timetable is now more colorful than ever. +- Improved "Classic" timetable color palette. +- A new page for customizing theme colors has been added. +- You can withdraw applications of second class activities. +- The Cache is no longer cleared when the device runs out of storage space. +- Other minor UI improvements and localizations. + +## v2.3.0+23 +- The 2048 game supports save/load feature. +- You can add, delete, and edit courses in the timetable. +- The Timetable is now more colorful than ever. +- Improved "Classic" timetable color palette. +- A new page for customizing theme color has been added. +- You can withdraw applications of second class activities. +- The Cache is no longer cleared when the device runs out of storage space. +- Other minor UI improvements and localizations. + +## v2.2.0+21 +- The Minesweeper game was introduced. +- Login-related error dialogs now have detailed description. +- [Bug fix] Wrong localization text. +- UI and performance improvement. + +## v2.1.3+19 +- More types of OA login errors can be recognized. +- Except for iOS and macOS, the app automatically checks update on startup. +- By following school changes, electricity price rises from 0.61 to 0.636. +- [iOS] It's supported to scan QR code generated from this app with Camera app. +- [Bug fix] In some cases, OA captcha is infinitely asking for input, but the login doesn't work. + +## v2.1.2+18 +- GPA calculator was added. +- Library searching and borrowing list were added. +- HTTP and HTTPS proxy settings page was added. +- The UI of some pages was redesigned. +- ICP licence was added. +- [Bug Fix] Applying for a second classroom activity would make a double-application. +- [Bug Fix] In some cases, it is not possible to load the activities already attended in the second class. diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..7e7e7f67d --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: From 7ac2aaab67a2813d969f48cba52297fcaeef3936 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 02:13:25 +0800 Subject: [PATCH 065/458] updated CHANGELOG.md --- CHANGELOG.md | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51086941a..2f6aab826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,39 +1,32 @@ # Changes -## v2.3.0+25 -- The 2048 game supports save/load feature. -- You can add, delete, and edit courses in the timetable. -- The Timetable is now more colorful than ever. -- Improved "Classic" timetable color palette. -- A new page for customizing theme colors has been added. -- You can withdraw applications of second class activities. -- The Cache is no longer cleared when the device runs out of storage space. -- Other minor UI improvements and localizations. +## v2.4.0 +- Redesigned the expense statistics page. -## v2.3.0+23 +## v2.3.0 - The 2048 game supports save/load feature. - You can add, delete, and edit courses in the timetable. - The Timetable is now more colorful than ever. - Improved "Classic" timetable color palette. -- A new page for customizing theme color has been added. +- A new page for customizing theme colors has been added. - You can withdraw applications of second class activities. - The Cache is no longer cleared when the device runs out of storage space. - Other minor UI improvements and localizations. -## v2.2.0+21 +## v2.2.0 - The Minesweeper game was introduced. - Login-related error dialogs now have detailed description. - [Bug fix] Wrong localization text. - UI and performance improvement. -## v2.1.3+19 +## v2.1.3 - More types of OA login errors can be recognized. - Except for iOS and macOS, the app automatically checks update on startup. - By following school changes, electricity price rises from 0.61 to 0.636. - [iOS] It's supported to scan QR code generated from this app with Camera app. - [Bug fix] In some cases, OA captcha is infinitely asking for input, but the login doesn't work. -## v2.1.2+18 +## v2.1.2 - GPA calculator was added. - Library searching and borrowing list were added. - HTTP and HTTPS proxy settings page was added. From 52d4b20bffe38ccad6c9e3689fd525626aaad640 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 13:21:18 +0800 Subject: [PATCH 066/458] [expense] [statistics] move summary out of pie chart --- lib/life/expense_records/page/statistics.dart | 21 +++++++++++++++++++ lib/life/expense_records/utils.dart | 2 +- .../expense_records/widget/chart/bar.dart | 2 +- .../expense_records/widget/chart/pie.dart | 14 +------------ 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 67d314eca..6722fd718 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -1,17 +1,21 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; +import 'package:sit/design/animation/animated.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; import 'package:sit/life/expense_records/storage/local.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/life/expense_records/utils.dart'; import 'package:sit/utils/collection.dart'; +import 'package:statistics/statistics.dart'; import '../entity/local.dart'; import '../i18n.dart'; import '../init.dart'; import '../widget/chart/bar.dart'; +import '../widget/chart/header.dart'; import '../widget/chart/pie.dart'; import '../widget/transaction.dart'; @@ -91,6 +95,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { }); }); final current = startTime2Records.indexAt(index); + final type2Records = current.records.groupListsBy((r) => r.type).entries.toList(); return Scaffold( appBar: AppBar( title: i18n.stats.title.text(), @@ -109,6 +114,22 @@ class _ExpenseStatisticsPageState extends ConsumerState { const Divider(), ExpensePieChart(records: current.records), const Divider(), + ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: type2Records.length, + itemBuilder: (ctx, i) { + final e = type2Records[i]; + final amounts = e.value.map((e) => e.deltaAmount).toList(); + return ExpenseAverageTile( + average: amounts.mean, + max: amounts.max, + type: e.key, + ); + }, + ), + const Divider(), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 38c9745a2..48d087403 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -131,7 +131,7 @@ double accumulateTransactionAmount(List transactions) { } ({double total, Map records, double total})> parts}) - separateTransactionByType( + statisticsTransactionByType( List records, ) { final type2transactions = records.groupListsBy((record) => record.type); diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 1382f9bac..c8a6e6e22 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -292,7 +292,7 @@ class AmountChartWidget extends StatelessWidget { ), groupsSpace: 40, barGroups: delegate.data.mapIndexed((i, records) { - final (:total, :parts) = separateTransactionByType(records); + final (:total, :parts) = statisticsTransactionByType(records); var c = 0.0; return BarChartGroupData( x: i, diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index d5762edd8..ecf0b3fb4 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -29,8 +29,7 @@ class _ExpensePieChartState extends State { @override Widget build(BuildContext context) { assert(widget.records.every((type) => type.isConsume)); - final (total: total, :parts) = separateTransactionByType(widget.records); - final ascending = parts.entries.sortedBy((e) => e.value.total).reversed.toList(); + final (:total, :parts) = statisticsTransactionByType(widget.records); return [ ExpensePieChartHeader( total: total, @@ -40,17 +39,6 @@ class _ExpensePieChartState extends State { child: buildChart(parts), ), buildLegends(parts).padAll(8).align(at: Alignment.topLeft), - const Divider(), - [ - ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), - ...ascending.map((e) { - final amounts = e.value.records.map((e) => e.deltaAmount).toList(); - return ExpenseAverageTile( - average: amounts.mean, - max: amounts.max, - type: e.key, - ); - })].column(caa: CrossAxisAlignment.start).animatedSized(), ].column(caa: CrossAxisAlignment.start); } From 2a3f96f185871fdc827f8afbefe15b428445618a Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 14:34:20 +0800 Subject: [PATCH 067/458] [expense] [statistics] StatisticsDelegate --- lib/design/widgets/grouped.dart | 2 + lib/life/expense_records/page/statistics.dart | 64 +++--- lib/life/expense_records/utils.dart | 4 +- .../expense_records/widget/chart/bar.dart | 196 ++---------------- .../widget/chart/delegate.dart | 178 ++++++++++++++++ .../expense_records/widget/chart/pie.dart | 29 ++- lib/life/expense_records/widget/group.dart | 2 +- lib/school/exam_result/page/gpa.dart | 2 +- lib/school/yellow_pages/widgets/list.dart | 2 +- 9 files changed, 250 insertions(+), 229 deletions(-) create mode 100644 lib/life/expense_records/widget/chart/delegate.dart diff --git a/lib/design/widgets/grouped.dart b/lib/design/widgets/grouped.dart index ddd954ef6..b14836072 100644 --- a/lib/design/widgets/grouped.dart +++ b/lib/design/widgets/grouped.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:sliver_tools/sliver_tools.dart'; typedef HeaderBuilder = Widget Function( + BuildContext context, bool expanded, VoidCallback toggleExpand, Widget defaultTrailing, @@ -37,6 +38,7 @@ class _GroupedSectionState extends State { child: Card( clipBehavior: Clip.hardEdge, child: widget.headerBuilder( + context, expanded, () { setState(() { diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 6722fd718..9d946c441 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/animation/animated.dart'; +import 'package:sit/design/widgets/grouped.dart'; import 'package:sit/life/expense_records/entity/statistics.dart'; import 'package:sit/life/expense_records/storage/local.dart'; import 'package:rettulf/rettulf.dart'; @@ -15,6 +15,7 @@ import '../entity/local.dart'; import '../i18n.dart'; import '../init.dart'; import '../widget/chart/bar.dart'; +import '../widget/chart/delegate.dart'; import '../widget/chart/header.dart'; import '../widget/chart/pie.dart'; import '../widget/transaction.dart'; @@ -95,29 +96,31 @@ class _ExpenseStatisticsPageState extends ConsumerState { }); }); final current = startTime2Records.indexAt(index); + assert(current.records.every((type) => type.isConsume)); final type2Records = current.records.groupListsBy((r) => r.type).entries.toList(); + final delegate = StatisticsDelegate.byMode( + mode, + start: current.start, + records: current.records, + ); return Scaffold( appBar: AppBar( title: i18n.stats.title.text(), ), body: [ - ListView( + CustomScrollView( controller: controller, physics: const AlwaysScrollableScrollPhysics(), - children: [ - buildModeSelector(mode).padSymmetric(h: 16, v: 4), - ExpenseBarChart( - start: current.start, - records: current.records, - mode: mode, - ), - const Divider(), - ExpensePieChart(records: current.records), - const Divider(), - ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverList.list(children: [ + buildModeSelector(mode).padSymmetric(h: 16, v: 4), + ExpenseBarChart(delegate: delegate), + const Divider(), + ExpensePieChart(delegate: delegate), + const Divider(), + ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), + ]), + SliverList.builder( itemCount: type2Records.length, itemBuilder: (ctx, i) { final e = type2Records[i]; @@ -129,19 +132,26 @@ class _ExpenseStatisticsPageState extends ConsumerState { ); }, ), - const Divider(), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: current.records.length, - itemBuilder: (ctx, i) { - final record = current.records[i]; - return TransactionTile(record); - }, - ), + SliverList.list(children: [ + const Divider(), + ExpenseChartHeaderLabel("Details").padFromLTRB(16, 8, 0, 0), + ]), + ...startTime2Records.map((e){ + return GroupedSection( + headerBuilder: (context,expanded, toggleExpand, defaultTrailing) { + return ListTile( + + ); + }, + itemCount: e.records.length, + itemBuilder: (ctx, i) { + final record = e.records[i]; + return TransactionTile(record); + }, + ); + }), ], ), - // ListView() $showTimeSpan >> (ctx, showTimeSpan) => AnimatedSlide( offset: showTimeSpan ? Offset.zero : const Offset(0, -2), diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 48d087403..d6d030da7 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -130,7 +130,7 @@ double accumulateTransactionAmount(List transactions) { return total; } -({double total, Map records, double total})> parts}) +({double total, Map records, double total})> type2Stats}) statisticsTransactionByType( List records, ) { @@ -139,7 +139,7 @@ double accumulateTransactionAmount(List transactions) { final total = type2total.values.sum; return ( total: total, - parts: type2transactions.map((type, records) { + type2Stats: type2transactions.map((type, records) { final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); }) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index c8a6e6e22..549a1e067 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -1,32 +1,20 @@ -import 'dart:math'; - -import 'package:collection/collection.dart' hide IterableDoubleExtension; -import 'package:easy_localization/easy_localization.dart'; +import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/route.dart'; import 'package:sit/utils/date.dart'; -import 'package:sit/utils/format.dart'; -import 'package:statistics/statistics.dart'; -import '../../entity/local.dart'; import '../../entity/statistics.dart'; -import "../../i18n.dart"; import '../../utils.dart'; +import 'delegate.dart'; import 'header.dart'; class ExpenseBarChart extends StatefulWidget { - final DateTime start; - final StatisticsMode mode; - final List records; + final StatisticsDelegate delegate; const ExpenseBarChart({ super.key, - required this.start, - required this.records, - required this.mode, + required this.delegate, }); @override @@ -34,16 +22,13 @@ class ExpenseBarChart extends StatefulWidget { } class _ExpenseBarChartState extends State { + StatisticsDelegate get delegate => widget.delegate; + DateTime get start => delegate.start; + StatisticsMode get mode => delegate.mode; @override Widget build(BuildContext context) { - assert(widget.records.every((type) => type.isConsume)); - final delegate = StatisticsDelegate.byMode( - widget.mode, - start: widget.start, - records: widget.records, - ); return [ - buildChartHeader(delegate).padFromLTRB(16, 8, 0, 8), + buildChartHeader().padFromLTRB(16, 8, 0, 8), AspectRatio( aspectRatio: 1.5, child: AmountChartWidget( @@ -53,10 +38,10 @@ class _ExpenseBarChartState extends State { ].column(caa: CrossAxisAlignment.start); } - Widget buildChartHeader(StatisticsDelegate delegate) { - final from = widget.start; - final to = widget.mode.getAfterUnitTime(start: widget.start, endLimit: DateTime.now()); - return switch (widget.mode) { + Widget buildChartHeader() { + final from = start; + final to = mode.getAfterUnitTime(start: start, endLimit: DateTime.now()); + return switch (mode) { StatisticsMode.year => ExpenseChartHeader( upper: "Monthly average", content: "¥${delegate.average.toStringAsFixed(2)}", @@ -71,159 +56,6 @@ class _ExpenseBarChartState extends State { } } -class StatisticsDelegate { - final List> data; - final double average; - final double total; - final StatisticsMode mode; - final Widget Function(BuildContext context, double value, TitleMeta meta) side; - final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; - - const StatisticsDelegate({ - required this.mode, - required this.data, - required this.average, - required this.total, - required this.side, - required this.bottom, - }); - - factory StatisticsDelegate.byMode( - StatisticsMode mode, { - required DateTime start, - required List records, - }) { - switch (mode) { - case StatisticsMode.week: - return StatisticsDelegate.week(start: start, records: records); - case StatisticsMode.month: - return StatisticsDelegate.month(start: start, records: records); - case StatisticsMode.year: - return StatisticsDelegate.year(start: start, records: records); - } - } - - factory StatisticsDelegate.week({ - required DateTime start, - required List records, - }) { - final now = DateTime.now(); - final data = List.generate( - start.year == now.year && start.week == now.week ? now.calendarOrderWeekday + 1 : 7, - (i) => [], - ); - for (final record in records) { - // add data at the same weekday. - // sunday goes first - data[record.timestamp.calendarOrderWeekday].add(record); - } - final amounts = records.map((r) => r.deltaAmount).toList(); - final dayTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); - return StatisticsDelegate( - mode: StatisticsMode.week, - data: data, - side: _buildSideTitle, - average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, - total: amounts.sum, - bottom: (ctx, value, mate) { - final index = value.toInt(); - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text( - style: ctx.textTheme.labelMedium, - Weekday.calendarOrder[index].l10nShort(), - ), - ); - }, - ); - } - - factory StatisticsDelegate.month({ - required DateTime start, - required List records, - }) { - final now = DateTime.now(); - final data = List.generate( - start.year == now.year && start.month == now.month ? now.day : daysInMonth(year: start.year, month: start.month), - (i) => [], - ); - for (final record in records) { - // add data on the same day. - data[record.timestamp.day - 1].add(record); - } - final amounts = records.map((r) => r.deltaAmount).toList(); - final dayTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); - final sep = data.length ~/ 5; - return StatisticsDelegate( - mode: StatisticsMode.week, - data: data, - average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, - total: amounts.sum, - side: _buildSideTitle, - bottom: (ctx, value, meta) { - final index = value.toInt(); - if (!(index == 0 || index == data.length - 1) && index % sep != 0) { - return const SizedBox(); - } - return SideTitleWidget( - axisSide: meta.axisSide, - child: Text( - style: ctx.textTheme.labelMedium, - "${index + 1}", - ), - ); - }, - ); - } - - factory StatisticsDelegate.year({ - required DateTime start, - required List records, - }) { - final _monthFormat = DateFormat.MMM($Key.currentContext!.locale.toString()); - final now = DateTime.now(); - final data = List.generate(start.year == now.year ? now.month : 12, (i) => []); - for (final record in records) { - // add data in the same month. - data[record.timestamp.month - 1].add(record); - } - final amounts = records.map((r) => r.deltaAmount).toList(); - final monthTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); - return StatisticsDelegate( - mode: StatisticsMode.week, - data: data, - average: monthTotals.isEmpty ? 0.0 : monthTotals.mean, - total: amounts.sum, - side: _buildSideTitle, - bottom: (ctx, value, mate) { - final index = value.toInt(); - final text = _monthFormat.format(DateTime(0, index + 1)); - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text( - style: ctx.textTheme.labelMedium, - text.substring(0, min(3, text.length)), - ), - ); - }, - ); - } - - static Widget _buildSideTitle(BuildContext ctx, double value, TitleMeta meta) { - String text = '¥${formatWithoutTrailingZeros(value)}'; - return SideTitleWidget( - axisSide: meta.axisSide, - child: Text( - style: ctx.textTheme.labelMedium, - text, - ), - ); - } -} - class AmountChartWidget extends StatelessWidget { final StatisticsDelegate delegate; @@ -292,14 +124,14 @@ class AmountChartWidget extends StatelessWidget { ), groupsSpace: 40, barGroups: delegate.data.mapIndexed((i, records) { - final (:total, :parts) = statisticsTransactionByType(records); + final (:total, :type2Stats) = statisticsTransactionByType(records); var c = 0.0; return BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: total, - rodStackItems: parts.entries.map((e) { + rodStackItems: type2Stats.entries.map((e) { final res = BarChartRodStackItem( c, c + e.value.total, diff --git a/lib/life/expense_records/widget/chart/delegate.dart b/lib/life/expense_records/widget/chart/delegate.dart new file mode 100644 index 000000000..a0147efca --- /dev/null +++ b/lib/life/expense_records/widget/chart/delegate.dart @@ -0,0 +1,178 @@ +import 'dart:math'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rettulf/rettulf.dart'; +import 'package:sit/l10n/time.dart'; +import 'package:sit/life/expense_records/utils.dart'; +import 'package:sit/route.dart'; +import 'package:sit/utils/date.dart'; +import 'package:sit/utils/format.dart'; +import 'package:statistics/statistics.dart'; + +import '../../entity/local.dart'; +import '../../entity/statistics.dart'; + +class StatisticsDelegate { + final List> data; + final double average; + final double total; + final Map records, double total})> type2Stats; + final StatisticsMode mode; + final DateTime start; + final Widget Function(BuildContext context, double value, TitleMeta meta) side; + final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; + + const StatisticsDelegate({ + required this.mode, + required this.data, + required this.type2Stats, + required this.start, + required this.average, + required this.total, + required this.side, + required this.bottom, + }); + + factory StatisticsDelegate.byMode( + StatisticsMode mode, { + required DateTime start, + required List records, + }) { + switch (mode) { + case StatisticsMode.week: + return StatisticsDelegate.week(start: start, records: records); + case StatisticsMode.month: + return StatisticsDelegate.month(start: start, records: records); + case StatisticsMode.year: + return StatisticsDelegate.year(start: start, records: records); + } + } + + factory StatisticsDelegate.week({ + required DateTime start, + required List records, + }) { + final now = DateTime.now(); + final data = List.generate( + start.year == now.year && start.week == now.week ? now.calendarOrderWeekday + 1 : 7, + (i) => [], + ); + for (final record in records) { + // add data at the same weekday. + // sunday goes first + data[record.timestamp.calendarOrderWeekday].add(record); + } + final (:total,:type2Stats) = statisticsTransactionByType(records); + final dayTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + return StatisticsDelegate( + mode: StatisticsMode.week, + start: start, + data: data, + side: _buildSideTitle, + average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, + type2Stats: type2Stats, + total: total, + bottom: (ctx, value, mate) { + final index = value.toInt(); + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + style: ctx.textTheme.labelMedium, + Weekday.calendarOrder[index].l10nShort(), + ), + ); + }, + ); + } + + factory StatisticsDelegate.month({ + required DateTime start, + required List records, + }) { + final now = DateTime.now(); + final data = List.generate( + start.year == now.year && start.month == now.month ? now.day : daysInMonth(year: start.year, month: start.month), + (i) => [], + ); + for (final record in records) { + // add data on the same day. + data[record.timestamp.day - 1].add(record); + } + final (:total,:type2Stats) = statisticsTransactionByType(records); + final dayTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + final sep = data.length ~/ 5; + return StatisticsDelegate( + mode: StatisticsMode.week, + start: start, + data: data, + average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, + type2Stats: type2Stats, + total: total, + side: _buildSideTitle, + bottom: (ctx, value, meta) { + final index = value.toInt(); + if (!(index == 0 || index == data.length - 1) && index % sep != 0) { + return const SizedBox(); + } + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + style: ctx.textTheme.labelMedium, + "${index + 1}", + ), + ); + }, + ); + } + + factory StatisticsDelegate.year({ + required DateTime start, + required List records, + }) { + final _monthFormat = DateFormat.MMM($Key.currentContext!.locale.toString()); + final now = DateTime.now(); + final data = List.generate(start.year == now.year ? now.month : 12, (i) => []); + for (final record in records) { + // add data in the same month. + data[record.timestamp.month - 1].add(record); + } + final (:total,:type2Stats) = statisticsTransactionByType(records); + final monthTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + return StatisticsDelegate( + mode: StatisticsMode.week, + start: start, + data: data, + average: monthTotals.isEmpty ? 0.0 : monthTotals.mean, + type2Stats: type2Stats, + total: total, + side: _buildSideTitle, + bottom: (ctx, value, mate) { + final index = value.toInt(); + final text = _monthFormat.format(DateTime(0, index + 1)); + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + style: ctx.textTheme.labelMedium, + text.substring(0, min(3, text.length)), + ), + ); + }, + ); + } + + static Widget _buildSideTitle(BuildContext ctx, double value, TitleMeta meta) { + String text = '¥${formatWithoutTrailingZeros(value)}'; + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + style: ctx.textTheme.labelMedium, + text, + ), + ); + } +} diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index ecf0b3fb4..689343845 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -3,20 +3,19 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/animation/animated.dart'; -import 'package:statistics/statistics.dart'; import '../../entity/local.dart'; +import '../../entity/statistics.dart'; import "../../i18n.dart"; -import '../../utils.dart'; +import 'delegate.dart'; import 'header.dart'; class ExpensePieChart extends StatefulWidget { - final List records; + final StatisticsDelegate delegate; const ExpensePieChart({ super.key, - required this.records, + required this.delegate, }); @override @@ -25,24 +24,24 @@ class ExpensePieChart extends StatefulWidget { class _ExpensePieChartState extends State { int touchedIndex = -1; - + StatisticsDelegate get delegate => widget.delegate; + DateTime get start => delegate.start; + StatisticsMode get mode => delegate.mode; @override Widget build(BuildContext context) { - assert(widget.records.every((type) => type.isConsume)); - final (:total, :parts) = statisticsTransactionByType(widget.records); return [ ExpensePieChartHeader( - total: total, + total: delegate.total, ).padFromLTRB(16, 8, 0, 0), AspectRatio( aspectRatio: 1.5, - child: buildChart(parts), + child: buildChart(), ), - buildLegends(parts).padAll(8).align(at: Alignment.topLeft), + buildLegends().padAll(8).align(at: Alignment.topLeft), ].column(caa: CrossAxisAlignment.start); } - Widget buildChart(Map records, double total})> parts) { + Widget buildChart() { return PieChart( PieChartData( pieTouchData: PieTouchData( @@ -63,7 +62,7 @@ class _ExpensePieChartState extends State { ), sectionsSpace: 0, centerSpaceRadius: 60, - sections: parts.entries.mapIndexed((i, entry) { + sections: delegate.type2Stats.entries.mapIndexed((i, entry) { final isTouched = i == touchedIndex; final MapEntry(key: type, value: (records: _, :total, :proportion)) = entry; final color = type.color.harmonizeWith(context.colorScheme.primary); @@ -81,8 +80,8 @@ class _ExpensePieChartState extends State { ); } - Widget buildLegends(Map records, double total})> parts) { - return parts.entries + Widget buildLegends() { + return delegate.type2Stats.entries .sortedBy((e) => -e.value.total) .map((record) { final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; diff --git a/lib/life/expense_records/widget/group.dart b/lib/life/expense_records/widget/group.dart index 669376430..99b935e8a 100644 --- a/lib/life/expense_records/widget/group.dart +++ b/lib/life/expense_records/widget/group.dart @@ -24,7 +24,7 @@ class TransactionGroupSection extends StatelessWidget { Widget build(BuildContext context) { final (:income, :outcome) = accumulateTransactionIncomeOutcome(records); return GroupedSection( - headerBuilder: (expanded, toggleExpand, defaultTrailing) { + headerBuilder: (context,expanded, toggleExpand, defaultTrailing) { return ListTile( title: context.formatYmText((time.toDateTime())).text(), titleTextStyle: context.textTheme.titleMedium, diff --git a/lib/school/exam_result/page/gpa.dart b/lib/school/exam_result/page/gpa.dart index 05bc1cfaf..db6d475ee 100644 --- a/lib/school/exam_result/page/gpa.dart +++ b/lib/school/exam_result/page/gpa.dart @@ -222,7 +222,7 @@ class _ExamResultGroupBySemesterState extends State { final isGroupNoneSelected = intersection.isEmpty; final isGroupAllSelected = intersection.length == indicesOfGroup.length; return GroupedSection( - headerBuilder: (expanded, toggleExpand, defaultTrailing) { + headerBuilder: (context, expanded, toggleExpand, defaultTrailing) { return ListTile( leading: Icon(expanded ? Icons.expand_less : Icons.expand_more), title: widget.semester.l10n().text(), diff --git a/lib/school/yellow_pages/widgets/list.dart b/lib/school/yellow_pages/widgets/list.dart index 92a8cd83c..f098f5c83 100644 --- a/lib/school/yellow_pages/widgets/list.dart +++ b/lib/school/yellow_pages/widgets/list.dart @@ -67,7 +67,7 @@ class _SchoolContactListState extends State { slivers: department2contacts.entries .mapIndexed( (i, entry) => GroupedSection( - headerBuilder: (expanded, toggleExpand, defaultTrailing) { + headerBuilder: (context,expanded, toggleExpand, defaultTrailing) { return ListTile( title: entry.key.text(), titleTextStyle: context.textTheme.titleMedium, From a3712a2c2ab09a67abdf828d58662edc070610e6 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 15:29:45 +0800 Subject: [PATCH 068/458] [expense] [statistics] StatisticsMode.day --- assets/l10n/en.yaml | 7 ++- assets/l10n/zh-Hans.yaml | 1 + assets/l10n/zh-Hant.yaml | 1 + .../expense_records/entity/statistics.dart | 25 +++++++- lib/life/expense_records/page/statistics.dart | 39 +++++++++---- lib/life/expense_records/utils.dart | 16 ++++-- .../expense_records/widget/chart/bar.dart | 6 ++ .../widget/chart/delegate.dart | 57 +++++++++++++++++-- lib/utils/date.dart | 8 +++ 9 files changed, 134 insertions(+), 26 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index bf38c21ae..1eb3a85a9 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -429,9 +429,10 @@ expenseRecords: refreshFailedTip: Failed to refresh expense records lastUpdateTime: "Last update: {}" statsMode: - week: Week - month: Month - year: Year + day: D + week: W + month: M + year: Y view: balance: Balance rmb: RMB diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index de121c088..34ae898e3 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -426,6 +426,7 @@ expenseRecords: refreshFailedTip: 消费记录刷新失败 lastUpdateTime: "上次更新:{}" statsMode: + day: 日 week: 周 month: 月 year: 年 diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index c8c57d319..613153029 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -426,6 +426,7 @@ expenseRecords: refreshFailedTip: 消費記錄重新整理失敗 lastUpdateTime: "最後更新:{}" statsMode: + day: 日 week: 周 month: 月 year: 年 diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index 32e4a68b9..af5d7b26f 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -7,15 +7,28 @@ import 'local.dart'; typedef StartTime2Records = List<({DateTime start, List records})>; enum StatisticsMode { - week(), - month(), - year(); + day, + week, + month, + year; + + StatisticsMode get downgrade { + if (this == day) throw RangeError.range(0, 1, StatisticsMode.values.length); + return StatisticsMode.values[index - 1]; + } const StatisticsMode(); /// Resort the records, separate them by start time, and sort them in DateTime ascending order. StartTime2Records resort(List records) { switch (this) { + case StatisticsMode.day: + final d2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.month, r.timestamp.day)); + final startTime2Records = d2records.entries + .map((entry) => (start: DateTime(entry.key.$1, entry.key.$2, entry.key.$3), records: entry.value)) + .toList(); + startTime2Records.sortBy((r) => r.start); + return startTime2Records; case StatisticsMode.week: final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); final startTime2Records = ym2records.entries @@ -45,6 +58,12 @@ enum StatisticsMode { DateTime? endLimit, }) { var end = switch (this) { + StatisticsMode.day => start.copyWith( + day: start.day, + hour: 23, + minute: 59, + second: 59, + ), StatisticsMode.week => start.copyWith( day: start.day + 6, hour: 23, diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 9d946c441..54940aae3 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -9,6 +9,7 @@ import 'package:sit/life/expense_records/storage/local.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/life/expense_records/utils.dart'; import 'package:sit/utils/collection.dart'; +import 'package:sit/utils/date.dart'; import 'package:statistics/statistics.dart'; import '../entity/local.dart'; @@ -40,7 +41,7 @@ final _statisticsMode = class _StatisticsModeNotifier extends AutoDisposeNotifier { @override - StatisticsMode build() => StatisticsMode.month; + StatisticsMode build() => StatisticsMode.week; void set(StatisticsMode mode) { state = mode; @@ -136,20 +137,34 @@ class _ExpenseStatisticsPageState extends ConsumerState { const Divider(), ExpenseChartHeaderLabel("Details").padFromLTRB(16, 8, 0, 0), ]), - ...startTime2Records.map((e){ - return GroupedSection( - headerBuilder: (context,expanded, toggleExpand, defaultTrailing) { - return ListTile( - - ); - }, - itemCount: e.records.length, + if (mode != StatisticsMode.day) + ...mode.downgrade.resort(current.records).map((e) { + return GroupedSection( + headerBuilder: (context, expanded, toggleExpand, defaultTrailing) { + return ListTile( + title: formatDateSpan( + from: e.start, + to: mode.downgrade.getAfterUnitTime(start: e.start), + ).text(), + onTap: toggleExpand, + trailing: defaultTrailing, + ); + }, + itemCount: e.records.length, + itemBuilder: (ctx, i) { + final record = e.records[i]; + return TransactionTile(record); + }, + ); + }) + else + SliverList.builder( + itemCount: current.records.length, itemBuilder: (ctx, i) { - final record = e.records[i]; + final record = current.records[i]; return TransactionTile(record); }, - ); - }), + ), ], ), $showTimeSpan >> diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index d6d030da7..fb14b996e 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -139,7 +139,7 @@ double accumulateTransactionAmount(List transactions) { final total = type2total.values.sum; return ( total: total, - type2Stats: type2transactions.map((type, records) { + type2Stats: type2transactions.map((type, records) { final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); }) @@ -151,11 +151,16 @@ String resolveTime4Display({ required StatisticsMode mode, required DateTime date, }) { - final monthFormat = DateFormat.MMMM(); - final yearMonthFormat = DateFormat.yMMMM(); - final yearFormat = DateFormat.y(); final now = DateTime.now(); switch (mode) { + case StatisticsMode.day: + final dayDiff = now.difference(date).inDays; + if (dayDiff == 0) { + return "Today"; + } else if (dayDiff == 1) { + return "Yesterday"; + } + return formatDateSpan(from: date, to: StatisticsMode.day.getAfterUnitTime(start: date)); case StatisticsMode.week: if (date.year == now.year) { final nowWeek = now.week; @@ -168,6 +173,8 @@ String resolveTime4Display({ } return formatDateSpan(from: date, to: StatisticsMode.week.getAfterUnitTime(start: date)); case StatisticsMode.month: + final yearMonthFormat = DateFormat.yMMMM(); + final monthFormat = DateFormat.MMMM(); if (date.year == now.year) { if (date.month == now.month) { return "This month"; @@ -180,6 +187,7 @@ String resolveTime4Display({ return yearMonthFormat.format(date); } case StatisticsMode.year: + final yearFormat = DateFormat.y(); if (date.year == now.year) { return "This year"; } else if (date.year == now.year - 1) { diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 549a1e067..7223dddf4 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/utils/date.dart'; @@ -42,6 +43,11 @@ class _ExpenseBarChartState extends State { final from = start; final to = mode.getAfterUnitTime(start: start, endLimit: DateTime.now()); return switch (mode) { + StatisticsMode.day => ExpenseChartHeader( + upper: "Hourly average", + content: "¥${delegate.average.toStringAsFixed(2)}", + lower: DateFormat.yMMMMd().format(from), + ), StatisticsMode.year => ExpenseChartHeader( upper: "Monthly average", content: "¥${delegate.average.toStringAsFixed(2)}", diff --git a/lib/life/expense_records/widget/chart/delegate.dart b/lib/life/expense_records/widget/chart/delegate.dart index a0147efca..a9352858c 100644 --- a/lib/life/expense_records/widget/chart/delegate.dart +++ b/lib/life/expense_records/widget/chart/delegate.dart @@ -15,11 +15,12 @@ import '../../entity/local.dart'; import '../../entity/statistics.dart'; class StatisticsDelegate { + final StatisticsMode mode; final List> data; + final Map records, double total})> type2Stats; + final StartTime2Records start2Records; final double average; final double total; - final Map records, double total})> type2Stats; - final StatisticsMode mode; final DateTime start; final Widget Function(BuildContext context, double value, TitleMeta meta) side; final Widget Function(BuildContext context, double value, TitleMeta meta) bottom; @@ -28,6 +29,7 @@ class StatisticsDelegate { required this.mode, required this.data, required this.type2Stats, + required this.start2Records, required this.start, required this.average, required this.total, @@ -41,6 +43,8 @@ class StatisticsDelegate { required List records, }) { switch (mode) { + case StatisticsMode.day: + return StatisticsDelegate.day(start: start, records: records); case StatisticsMode.week: return StatisticsDelegate.week(start: start, records: records); case StatisticsMode.month: @@ -50,6 +54,48 @@ class StatisticsDelegate { } } + factory StatisticsDelegate.day({ + required DateTime start, + required List records, + }) { + final now = DateTime.now(); + final data = List.generate( + now.inTheSameDay(start) ? now.hour : 24, + (i) => [], + ); + for (final record in records) { + // add data at the same weekday. + // sunday goes first + data[record.timestamp.hour].add(record); + } + final (:total,:type2Stats) = statisticsTransactionByType(records); + final dayTotals = + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + return StatisticsDelegate( + mode: StatisticsMode.day, + start: start, + start2Records: StatisticsMode.day.resort(records), + data: data, + side: _buildSideTitle, + average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, + type2Stats: type2Stats, + total: total, + bottom: (ctx, value, mate) { + final index = value.toInt(); + if (!(index == 0 || index == data.length - 1) && index % 4 != 0) { + return const SizedBox(); + } + return SideTitleWidget( + axisSide: mate.axisSide, + child: Text( + style: ctx.textTheme.labelMedium, + "${index}", + ), + ); + }, + ); + } + factory StatisticsDelegate.week({ required DateTime start, required List records, @@ -70,6 +116,7 @@ class StatisticsDelegate { return StatisticsDelegate( mode: StatisticsMode.week, start: start, + start2Records: StatisticsMode.week.resort(records), data: data, side: _buildSideTitle, average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, @@ -106,8 +153,9 @@ class StatisticsDelegate { data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); final sep = data.length ~/ 5; return StatisticsDelegate( - mode: StatisticsMode.week, + mode: StatisticsMode.month, start: start, + start2Records: StatisticsMode.month.resort(records), data: data, average: dayTotals.isEmpty ? 0.0 : dayTotals.mean, type2Stats: type2Stats, @@ -144,8 +192,9 @@ class StatisticsDelegate { final monthTotals = data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); return StatisticsDelegate( - mode: StatisticsMode.week, + mode: StatisticsMode.year, start: start, + start2Records: StatisticsMode.year.resort(records), data: data, average: monthTotals.isEmpty ? 0.0 : monthTotals.mean, type2Stats: type2Stats, diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 81e6908df..b373f6cd2 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -67,6 +67,10 @@ extension DateTimeX on DateTime { final w = weekday; return w == DateTime.sunday ? 0 : w; } + + bool inTheSameDay(DateTime b) { + return year == b.year && month == b.month && day == b.day; + } } DateTime getDateOfFirstDayInWeek({ @@ -83,6 +87,10 @@ String formatDateSpan({ bool showYear = true, }) { final local = $Key.currentContext?.locale.toString(); + if (from.inTheSameDay(to)) { + final day = DateFormat.yMMMMd(local); + return day.format(from); + } final year = DateFormat.y(local); if (from.year == to.year) { final month = DateFormat.MMMM(local); From a8bf7f4661a16cd3ba5e980b1f7343a282da3101 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 15:35:43 +0800 Subject: [PATCH 069/458] bump freezed to 1.5.2 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 5d5532dc5..09ebd4540 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -889,10 +889,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "91bce569d4805ea5bad6619a3e8690df8ad062a235165af4c0c5d928dda15eaf" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 703766540..335b6403c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -165,7 +165,7 @@ dev_dependencies: build_runner: ^2.4.9 json_serializable: ^6.7.1 hive_generator: ^2.0.0 - freezed: ^2.5.1 + freezed: ^2.5.2 copy_with_extension_gen: ^5.0.4 test: ^1.24.9 From 24aeb4397259f4c879506ab55468f22658b65696 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 11 Apr 2024 16:14:51 +0800 Subject: [PATCH 070/458] [expense] [statistics] i18n --- assets/l10n/en.yaml | 17 ++++++- assets/l10n/zh-Hans.yaml | 17 ++++++- assets/l10n/zh-Hant.yaml | 17 ++++++- lib/life/expense_records/i18n.dart | 48 ++++++++++++++++++- lib/life/expense_records/page/statistics.dart | 4 +- lib/life/expense_records/utils.dart | 18 +++---- .../expense_records/widget/chart/bar.dart | 19 ++++---- .../widget/chart/delegate.dart | 2 +- .../expense_records/widget/chart/pie.dart | 6 +-- 9 files changed, 120 insertions(+), 28 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 1eb3a85a9..3ffe4003a 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -438,8 +438,23 @@ expenseRecords: rmb: RMB stats: title: Statistics - categories: Categories total: Total + summery: Summery + details: Details + averageSpendIn: "Average spent {amount} in {type}" + maxSpendOf: "with a max spend of {amount}" + averageLineLabel: avg + hourlyAverage: Hourly average + dailyAverage: daily average + monthlyAverage: Monthly average + today: Today + yesterday: Yesterday + thisWeek: This week + lastWeek: Last week + thisMonth: This month + lastMonth: Last month + thisYear: This year + lastYear: Last year type: consume: Consume coffee: Café diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index 34ae898e3..9d0635540 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -435,8 +435,23 @@ expenseRecords: rmb: 元 stats: title: 消费统计 - categories: 消费名目 total: 总计 + summary: 摘要 + details: 详情 + averageSpendIn: "在 {type} 上平均消费 {amount}" + maxSpendOf: "最大消费为 {amount}" + averageLineLabel: 平均 + hourlyAverage: 时均 + dailyAverage: 日均 + monthlyAverage: 月均 + today: 今天 + yesterday: 昨天 + thisWeek: 本周 + lastWeek: 上周 + thisMonth: 本月 + lastMonth: 上月 + thisYear: 今年 + lastYear: 去年 type: consume: 消费 coffee: 咖啡 diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index 613153029..a84d74a7d 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -435,8 +435,23 @@ expenseRecords: rmb: 元 stats: title: 消費統計 - categories: 消費類別 total: 總計 + summery: 摘要 + details: 詳細資料 + averageSpendIn: "在 {type} 上,平均消費 {amount}" + maxSpendOf: "最大消費為 {amount}" + averageLineLabel: 平均 + hourlyAverage: 小時平均 + dailyAverage: 日平均 + monthlyAverage: 月平均 + today: 今日 + yesterday: 昨日 + thisWeek: 本週 + lastWeek: 上週 + thisMonth: 本月 + lastMonth: 上月 + thisYear: 今年 + lastYear: 去年 type: consume: 消費 coffee: 咖啡 diff --git a/lib/life/expense_records/i18n.dart b/lib/life/expense_records/i18n.dart index b03e9654d..d1bedc077 100644 --- a/lib/life/expense_records/i18n.dart +++ b/lib/life/expense_records/i18n.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:sit/l10n/common.dart'; +import 'entity/local.dart'; + const i18n = _I18n(); class _I18n with CommonI18nMixin { @@ -44,9 +46,51 @@ class _Stats { String get title => "$ns.title".tr(); - String get categories => "$ns.categories".tr(); - String get total => "$ns.total".tr(); + + String get summary => "$ns.summary".tr(); + + String get details => "$ns.details".tr(); + + String averageSpendIn({ + required String amount, + required TransactionType type, + }) => + "$ns.averageSpendIn".tr(namedArgs: { + "amount": amount, + "type": type.l10n(), + }); + + String maxSpendOf({ + required String amount, + }) => + "$ns.maxSpendOf".tr(namedArgs: { + "amount": amount, + }); + + String get averageLineLabel => "$ns.averageLineLabel".tr(); + + String get hourlyAverage => "$ns.hourlyAverage".tr(); + + String get dailyAverage => "$ns.dailyAverage".tr(); + + String get monthlyAverage => "$ns.monthlyAverage".tr(); + + String get today => "$ns.today".tr(); + + String get yesterday => "$ns.yesterday".tr(); + + String get thisWeek => "$ns.thisWeek".tr(); + + String get lastWeek => "$ns.lastWeek".tr(); + + String get thisMonth => "$ns.thisMonth".tr(); + + String get lastMonth => "$ns.lastMonth".tr(); + + String get thisYear => "$ns.thisYear".tr(); + + String get lastYear => "$ns.lastYear".tr(); } class _View { diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index 54940aae3..f167bb91b 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -119,7 +119,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { const Divider(), ExpensePieChart(delegate: delegate), const Divider(), - ExpenseChartHeaderLabel("Summary").padFromLTRB(16, 8, 0, 0), + ExpenseChartHeaderLabel(i18n.stats.summary).padFromLTRB(16, 8, 0, 0), ]), SliverList.builder( itemCount: type2Records.length, @@ -135,7 +135,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { ), SliverList.list(children: [ const Divider(), - ExpenseChartHeaderLabel("Details").padFromLTRB(16, 8, 0, 0), + ExpenseChartHeaderLabel(i18n.stats.details).padFromLTRB(16, 8, 0, 0), ]), if (mode != StatisticsMode.day) ...mode.downgrade.resort(current.records).map((e) { diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index fb14b996e..44940de05 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -2,7 +2,6 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:sit/l10n/extension.dart'; import 'package:sit/life/expense_records/entity/local.dart'; import 'package:sit/school/utils.dart'; @@ -10,6 +9,7 @@ import 'package:sit/utils/date.dart'; import 'entity/remote.dart'; import 'entity/statistics.dart'; +import 'i18n.dart'; const deviceName2Type = { '开水': TransactionType.water, @@ -156,9 +156,9 @@ String resolveTime4Display({ case StatisticsMode.day: final dayDiff = now.difference(date).inDays; if (dayDiff == 0) { - return "Today"; + return i18n.stats.today; } else if (dayDiff == 1) { - return "Yesterday"; + return i18n.stats.yesterday; } return formatDateSpan(from: date, to: StatisticsMode.day.getAfterUnitTime(start: date)); case StatisticsMode.week: @@ -166,9 +166,9 @@ String resolveTime4Display({ final nowWeek = now.week; final dateWeek = date.week; if (dateWeek == nowWeek) { - return "This week"; + return i18n.stats.thisWeek; } else if (dateWeek == nowWeek - 1) { - return "Last week"; + return i18n.stats.lastWeek; } } return formatDateSpan(from: date, to: StatisticsMode.week.getAfterUnitTime(start: date)); @@ -177,9 +177,9 @@ String resolveTime4Display({ final monthFormat = DateFormat.MMMM(); if (date.year == now.year) { if (date.month == now.month) { - return "This month"; + return i18n.stats.thisMonth; } else if (date.month == now.month - 1) { - return "Last month"; + return i18n.stats.thisMonth; } else { return monthFormat.format(date); } @@ -189,9 +189,9 @@ String resolveTime4Display({ case StatisticsMode.year: final yearFormat = DateFormat.y(); if (date.year == now.year) { - return "This year"; + return i18n.stats.thisYear; } else if (date.year == now.year - 1) { - return "Last year"; + return i18n.stats.lastYear; } else { return yearFormat.format(date); } diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 7223dddf4..308fcd3d8 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -7,6 +7,7 @@ import 'package:sit/utils/date.dart'; import '../../entity/statistics.dart'; import '../../utils.dart'; +import "../../i18n.dart"; import 'delegate.dart'; import 'header.dart'; @@ -24,8 +25,11 @@ class ExpenseBarChart extends StatefulWidget { class _ExpenseBarChartState extends State { StatisticsDelegate get delegate => widget.delegate; + DateTime get start => delegate.start; + StatisticsMode get mode => delegate.mode; + @override Widget build(BuildContext context) { return [ @@ -44,17 +48,17 @@ class _ExpenseBarChartState extends State { final to = mode.getAfterUnitTime(start: start, endLimit: DateTime.now()); return switch (mode) { StatisticsMode.day => ExpenseChartHeader( - upper: "Hourly average", - content: "¥${delegate.average.toStringAsFixed(2)}", - lower: DateFormat.yMMMMd().format(from), - ), + upper: i18n.stats.hourlyAverage, + content: "¥${delegate.average.toStringAsFixed(2)}", + lower: DateFormat.yMMMMd().format(from), + ), StatisticsMode.year => ExpenseChartHeader( - upper: "Monthly average", + upper: i18n.stats.monthlyAverage, content: "¥${delegate.average.toStringAsFixed(2)}", lower: formatDateSpan(from: from, to: to), ), StatisticsMode.week || StatisticsMode.month => ExpenseChartHeader( - upper: "Daily average", + upper: i18n.stats.dailyAverage, content: "¥${delegate.average.toStringAsFixed(2)}", lower: formatDateSpan(from: from, to: to), ), @@ -107,7 +111,7 @@ class AmountChartWidget extends StatelessWidget { show: true, alignment: Alignment.bottomRight, style: context.textTheme.labelSmall, - labelResolver: (line) => "avg", + labelResolver: (line) => i18n.stats.averageLineLabel, ), y: delegate.average, strokeWidth: 3, @@ -154,4 +158,3 @@ class AmountChartWidget extends StatelessWidget { ); } } - diff --git a/lib/life/expense_records/widget/chart/delegate.dart b/lib/life/expense_records/widget/chart/delegate.dart index a9352858c..1390e346c 100644 --- a/lib/life/expense_records/widget/chart/delegate.dart +++ b/lib/life/expense_records/widget/chart/delegate.dart @@ -70,7 +70,7 @@ class StatisticsDelegate { } final (:total,:type2Stats) = statisticsTransactionByType(records); final dayTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).toList(); return StatisticsDelegate( mode: StatisticsMode.day, start: start, diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index 689343845..9fc7bc287 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -108,7 +108,7 @@ class ExpensePieChartHeader extends StatelessWidget { @override Widget build(BuildContext context) { return ExpenseChartHeader( - upper: "Total", + upper: i18n.stats.total, content: "¥${total.toStringAsFixed(2)}", ); } @@ -130,8 +130,8 @@ class ExpenseAverageTile extends StatelessWidget { Widget build(BuildContext context) { return ListTile( leading: Icon(type.icon, color: type.color), - title: "Average spent ¥${average.toStringAsFixed(2)} in ${type.l10n()}".text(), - subtitle: "with a max spend of ¥${max.toStringAsFixed(2)}".text(), + title: i18n.stats.averageSpendIn(amount: "¥${average.toStringAsFixed(2)}", type: type).text(), + subtitle: i18n.stats.maxSpendOf(amount: "¥${max.toStringAsFixed(2)}").text(), ); } } From d68cd04478f1a98b48fd90ca94d4362c4bfaab62 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Apr 2024 08:42:14 +0000 Subject: [PATCH 071/458] build: 2.4.0+28 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 335b6403c..59da3a73e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A multiplatform app for SIT students." # The build version numbers is incremented automatically. # DO NOT DIRECTLY CHANGE IT -version: 2.4.0+27 +version: 2.4.0+28 homepage: https://github.com/liplum/mimir repository: https://github.com/liplum/mimir From a1791484c2c8a1f724068f979338bd3e34cdeaa6 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 00:26:24 +0800 Subject: [PATCH 072/458] [android] keep target sdk pace with Flutter --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5153e5df8..77c580c05 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -50,7 +50,7 @@ android { defaultConfig { applicationId "life.mysit.sit_life" minSdkVersion 23 - targetSdkVersion 33 + targetSdkVersion flutter.compileSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } From 7674140f0d01dfa57c12b9fae4487bda40baf6fb Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 00:28:57 +0800 Subject: [PATCH 073/458] [expense] [statistics] fixed l10n --- assets/l10n/en.yaml | 2 +- lib/r.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 3ffe4003a..812ead088 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -439,7 +439,7 @@ expenseRecords: stats: title: Statistics total: Total - summery: Summery + summary: Summary details: Details averageSpendIn: "Average spent {amount} in {type}" maxSpendOf: "with a max spend of {amount}" diff --git a/lib/r.dart b/lib/r.dart index 3c33ac10b..e0512a51d 100644 --- a/lib/r.dart +++ b/lib/r.dart @@ -44,7 +44,7 @@ class R { static const enLocale = Locale('en'); static const zhHansLocale = Locale.fromSubtags(languageCode: "zh", scriptCode: "Hans"); static const zhHantLocale = Locale.fromSubtags(languageCode: "zh", scriptCode: "Hant"); - static const defaultLocale = zhHansLocale; + static const defaultLocale = enLocale; static const supportedLocales = [ enLocale, zhHansLocale, From 84c5abc9d9871a75bd21a8f53eb15c6d5a60ff4c Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 01:39:20 +0800 Subject: [PATCH 074/458] [dev] login demo account --- lib/credentials/entity/credential.dart | 4 +-- lib/login/page/index.dart | 10 +++---- lib/r.dart | 4 +-- lib/settings/page/developer.dart | 41 +++++++++++++++++++------- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/lib/credentials/entity/credential.dart b/lib/credentials/entity/credential.dart index a2436cf08..5aa7be25f 100644 --- a/lib/credentials/entity/credential.dart +++ b/lib/credentials/entity/credential.dart @@ -11,7 +11,7 @@ class Credentials { @HiveField(1) final String password; - Credentials({ + const Credentials({ required this.account, required this.password, }); @@ -28,5 +28,5 @@ class Credentials { } @override - int get hashCode => toString().hashCode; + int get hashCode => Object.hash(account, password); } diff --git a/lib/login/page/index.dart b/lib/login/page/index.dart index dff174f4d..249c46fdf 100644 --- a/lib/login/page/index.dart +++ b/lib/login/page/index.dart @@ -41,8 +41,8 @@ class LoginPage extends ConsumerStatefulWidget { } class _LoginPageState extends ConsumerState { - final $account = TextEditingController(text: Dev.demoMode ? R.demoModeOaAccount : null); - final $password = TextEditingController(text: Dev.demoMode ? R.demoModeOaPassword : null); + final $account = TextEditingController(text: Dev.demoMode ? R.demoModeOaCredentials.account : null); + final $password = TextEditingController(text: Dev.demoMode ? R.demoModeOaCredentials.password : null); final _formKey = GlobalKey(); bool isPasswordClear = false; bool isLoggingIn = false; @@ -73,7 +73,7 @@ class _LoginPageState extends ConsumerState { Future login() async { final account = $account.text; final password = $password.text; - if (account == R.demoModeOaAccount && password == R.demoModeOaPassword) { + if (account == R.demoModeOaCredentials.account && password == R.demoModeOaCredentials.password) { await loginDemoMode(); } else { await loginWithCredentials(account, password, formatValid: (_formKey.currentState as FormState).validate()); @@ -86,7 +86,7 @@ class _LoginPageState extends ConsumerState { final rand = Random(); await Future.delayed(Duration(milliseconds: rand.nextInt(2000))); Settings.lastSignature ??= "Liplum"; - CredentialsInit.storage.oaCredentials = Credentials(account: R.demoModeOaAccount, password: R.demoModeOaPassword); + CredentialsInit.storage.oaCredentials = R.demoModeOaCredentials; CredentialsInit.storage.oaLoginStatus = LoginStatus.validated; CredentialsInit.storage.oaLastAuthTime = DateTime.now(); CredentialsInit.storage.oaUserType = OaUserType.undergraduate; @@ -97,7 +97,7 @@ class _LoginPageState extends ConsumerState { context.go("/"); } - /// 用户点击登录按钮后 + /// After the user clicks the login button Future loginWithCredentials( String account, String password, { diff --git a/lib/r.dart b/lib/r.dart index e0512a51d..939fda5da 100644 --- a/lib/r.dart +++ b/lib/r.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; +import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/school/yellow_pages/entity/contact.dart'; import 'package:sit/entity/version.dart'; @@ -29,8 +30,7 @@ class R { static const Size minWindowSize = Size(300, 400); static const eduEmailDomain = "mail.sit.edu.cn"; - static const demoModeOaAccount = "2300421153"; - static const demoModeOaPassword = "liplum-sit-life"; + static const demoModeOaCredentials = Credentials(account: "2300421153", password: "liplum-sit-life"); static const iosAppId = "6468989112"; static const iosAppStoreUrl = "https://apps.apple.com/cn/app/$iosAppId"; diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index b998b4874..1cfb05687 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -5,6 +5,8 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/entity/credential.dart'; +import 'package:sit/credentials/entity/login_status.dart'; +import 'package:sit/credentials/entity/user_type.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/credentials/utils.dart'; import 'package:sit/design/adaptive/dialog.dart'; @@ -14,9 +16,11 @@ import 'package:sit/design/widgets/expansion_tile.dart'; import 'package:sit/init.dart'; import 'package:sit/login/aggregated.dart'; import 'package:sit/login/utils.dart'; +import 'package:sit/r.dart'; import 'package:sit/settings/dev.dart'; import 'package:sit/design/widgets/navigation.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/settings/settings.dart'; import '../i18n.dart'; class DeveloperOptionsPage extends ConsumerStatefulWidget { @@ -31,7 +35,8 @@ class DeveloperOptionsPage extends ConsumerStatefulWidget { class _DeveloperOptionsPageState extends ConsumerState { @override Widget build(BuildContext context) { - final oaCredentials = CredentialsInit.storage.oaCredentials; + final credentials = ref.watch(CredentialsInit.storage.$oaCredentials); + final demoMode = ref.watch(Dev.$demoMode); return Scaffold( body: CustomScrollView( physics: const RangeMaintainingScrollPhysics(), @@ -54,9 +59,25 @@ class _DeveloperOptionsPageState extends ConsumerState { ), buildReload(), const DebugExpenseUserOverrideTile(), - if (oaCredentials != null) + if (credentials != null) SwitchOaUserTile( - currentCredentials: oaCredentials, + currentCredentials: credentials, + ), + if (demoMode && credentials != R.demoModeOaCredentials) + ListTile( + leading: const Icon(Icons.adb), + title: "Login demo account".text(), + trailing: const Icon(Icons.login), + onTap: () async { + Settings.lastSignature ??= "Liplum"; + CredentialsInit.storage.oaCredentials = R.demoModeOaCredentials; + CredentialsInit.storage.oaLoginStatus = LoginStatus.validated; + CredentialsInit.storage.oaLastAuthTime = DateTime.now(); + CredentialsInit.storage.oaUserType = OaUserType.undergraduate; + await Init.initModules(); + if (!context.mounted) return; + context.go("/"); + }, ), const DebugGoRouteTile(), ]), @@ -83,6 +104,7 @@ class _DeveloperOptionsPageState extends ConsumerState { Widget buildDemoModeToggle() { final demoMode = ref.watch(Dev.$demoMode); return ListTile( + leading: const Icon(Icons.adb), title: i18n.dev.demoMode.text(), trailing: Switch.adaptive( value: demoMode, @@ -141,13 +163,12 @@ class _DebugGoRouteTileState extends State { ), trailing: [ $route >> - (ctx, route) => - PlatformIconButton( + (ctx, route) => PlatformIconButton( onPressed: route.text.isEmpty ? null : () { - context.push(route.text); - }, + context.push(route.text); + }, icon: const Icon(Icons.arrow_forward), ) ].row(mas: MainAxisSize.min), @@ -183,9 +204,9 @@ class _SwitchOaUserTileState extends State { leading: const Icon(Icons.swap_horiz), trailing: isLoggingIn ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator.adaptive(), - ) + padding: EdgeInsets.all(8), + child: CircularProgressIndicator.adaptive(), + ) : null, children: [ ...credentialsList.map(buildCredentialsHistoryTile), From 1b3682a124985a54c721ba888c7a355e18b8cad8 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 12:22:15 +0800 Subject: [PATCH 075/458] initStorage --- lib/credentials/init.dart | 5 ++++- lib/init.dart | 19 +++++++++++++++++++ lib/life/electricity/init.dart | 2 ++ lib/life/expense_records/init.dart | 2 ++ lib/login/init.dart | 2 ++ lib/main.dart | 1 + lib/me/edu_email/init.dart | 4 +++- lib/school/class2nd/init.dart | 6 ++++-- lib/school/exam_arrange/init.dart | 2 ++ lib/school/exam_result/init.dart | 2 ++ lib/school/library/init.dart | 4 ++-- lib/school/oa_announce/init.dart | 2 ++ lib/school/yellow_pages/init.dart | 2 ++ lib/school/ywb/init.dart | 4 +++- lib/timetable/init.dart | 3 +++ lib/update/init.dart | 3 +++ 16 files changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/credentials/init.dart b/lib/credentials/init.dart index b84a67b4d..bc8c68a89 100644 --- a/lib/credentials/init.dart +++ b/lib/credentials/init.dart @@ -9,7 +9,6 @@ class CredentialsInit { static late CredentialStorage storage; static void init() { - storage = CredentialStorage(); Editor.registerEditor((ctx, desc, initial) => StringsEditor( fields: [ (name: "account", initial: initial.account), @@ -21,4 +20,8 @@ class CredentialsInit { EditorEx.registerEnumEditor(LoginStatus.values); EditorEx.registerEnumEditor(OaUserType.values); } + + static void initStorage() { + storage = CredentialStorage(); + } } diff --git a/lib/init.dart b/lib/init.dart index a8e1e6169..f514c8895 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -126,6 +126,25 @@ class Init { LoginInit.init(); } + static Future initStorage() async { + CredentialsInit.initStorage(); + TimetableInit.initStorage(); + if (!kIsWeb) { + UpdateInit.initStorage(); + OaAnnounceInit.initStorage(); + ExamResultInit.initStorage(); + ExamArrangeInit.initStorage(); + ExpenseRecordsInit.initStorage(); + LibraryInit.initStorage(); + YwbInit.initStorage(); + Class2ndInit.initStorage(); + ElectricityBalanceInit.initStorage(); + } + YellowPagesInit.initStorage(); + EduEmailInit.initStorage(); + LoginInit.initStorage(); + } + static void registerCustomEditor() { EditorEx.registerEnumEditor(Campus.values); EditorEx.registerEnumEditor(ThemeMode.values); diff --git a/lib/life/electricity/init.dart b/lib/life/electricity/init.dart index 47fc66cbf..80a1ae7d7 100644 --- a/lib/life/electricity/init.dart +++ b/lib/life/electricity/init.dart @@ -10,6 +10,8 @@ class ElectricityBalanceInit { static void init() { service = Dev.demoMode ? const DemoElectricityService() : const ElectricityService(); + } + static void initStorage() { storage = ElectricityStorage(); } } diff --git a/lib/life/expense_records/init.dart b/lib/life/expense_records/init.dart index c7646147c..d393acd08 100644 --- a/lib/life/expense_records/init.dart +++ b/lib/life/expense_records/init.dart @@ -10,6 +10,8 @@ class ExpenseRecordsInit { static void init() { service = Dev.demoMode ? const DemoExpenseService() : const ExpenseService(); + } + static void initStorage() { storage = ExpenseStorage(); } } diff --git a/lib/login/init.dart b/lib/login/init.dart index 79ddd076d..df242163d 100644 --- a/lib/login/init.dart +++ b/lib/login/init.dart @@ -6,4 +6,6 @@ class LoginInit { static void init() { authServerService = const AuthServerService(); } + static void initStorage() { + } } diff --git a/lib/main.dart b/lib/main.dart index 29408b101..6647a4b2f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -95,6 +95,7 @@ void main() async { Init.registerCustomEditor(); HttpOverrides.global = SitHttpOverrides(); await Init.initNetwork(); + await Init.initStorage(); await Init.initModules(); runApp( EasyLocalization( diff --git a/lib/me/edu_email/init.dart b/lib/me/edu_email/init.dart index 28cdcc349..324874392 100644 --- a/lib/me/edu_email/init.dart +++ b/lib/me/edu_email/init.dart @@ -6,7 +6,9 @@ class EduEmailInit { static late MailService service; static void init() { - storage = const EduEmailStorage(); service = MailService(); } + static void initStorage() { + storage = const EduEmailStorage(); + } } diff --git a/lib/school/class2nd/init.dart b/lib/school/class2nd/init.dart index 06fdb3b40..6ea7ec421 100644 --- a/lib/school/class2nd/init.dart +++ b/lib/school/class2nd/init.dart @@ -18,9 +18,11 @@ class Class2ndInit { static void init() { pointService = Dev.demoMode ? const DemoClass2ndPointsService() : const Class2ndPointsService(); - pointStorage = Class2ndPointsStorage(); activityService = Dev.demoMode ? const DemoClass2ndActivityService() : const Class2ndActivityService(); - activityStorage = const Class2ndActivityStorage(); applicationService = Dev.demoMode ? const DemoClass2ndApplicationService() : const Class2ndApplicationService(); } + static void initStorage() { + pointStorage = Class2ndPointsStorage(); + activityStorage = const Class2ndActivityStorage(); + } } diff --git a/lib/school/exam_arrange/init.dart b/lib/school/exam_arrange/init.dart index 3fc2ac529..e6a54e7fd 100644 --- a/lib/school/exam_arrange/init.dart +++ b/lib/school/exam_arrange/init.dart @@ -10,6 +10,8 @@ class ExamArrangeInit { static void init() { service = Dev.demoMode ? const DemoExamArrangeService() : const ExamArrangeService(); + } + static void initStorage() { storage = ExamArrangeStorage(); } } diff --git a/lib/school/exam_result/init.dart b/lib/school/exam_result/init.dart index 7113afc31..acc96eac8 100644 --- a/lib/school/exam_result/init.dart +++ b/lib/school/exam_result/init.dart @@ -16,6 +16,8 @@ class ExamResultInit { static void init() { ugService = Dev.demoMode ? const DemoExamResultUgService() : const ExamResultUgService(); pgService = Dev.demoMode ? const DemoExamResultPgService() : const ExamResultPgService(); + } + static void initStorage() { ugStorage = ExamResultUgStorage(); pgStorage = ExamResultPgStorage(); } diff --git a/lib/school/library/init.dart b/lib/school/library/init.dart index eea9d6db9..f2d10f758 100644 --- a/lib/school/library/init.dart +++ b/lib/school/library/init.dart @@ -42,9 +42,9 @@ class LibraryInit { bookImageSearch = const BookImageSearchService(); collectionPreviewService = const LibraryCollectionPreviewService(); hotSearchService = Dev.demoMode ? const DemoLibraryTrendsService() : const LibraryTrendsService(); - borrowService = const LibraryBorrowService(); - + } + static void initStorage() { searchStorage = const LibrarySearchStorage(); bookStorage = const LibraryBookStorage(); borrowStorage = LibraryBorrowStorage(); diff --git a/lib/school/oa_announce/init.dart b/lib/school/oa_announce/init.dart index 9f51d4325..d1ad3d513 100644 --- a/lib/school/oa_announce/init.dart +++ b/lib/school/oa_announce/init.dart @@ -11,6 +11,8 @@ class OaAnnounceInit { static void init() { service = Dev.demoMode ? const DemoOaAnnounceService() : const OaAnnounceService(); + } + static void initStorage() { storage = const OaAnnounceStorage(); } } diff --git a/lib/school/yellow_pages/init.dart b/lib/school/yellow_pages/init.dart index fe403b871..b4a90aaa4 100644 --- a/lib/school/yellow_pages/init.dart +++ b/lib/school/yellow_pages/init.dart @@ -4,6 +4,8 @@ class YellowPagesInit { static late YellowPagesStorage storage; static void init() { + } + static void initStorage() { storage = const YellowPagesStorage(); } } diff --git a/lib/school/ywb/init.dart b/lib/school/ywb/init.dart index 58039f7de..dc1d44d8e 100644 --- a/lib/school/ywb/init.dart +++ b/lib/school/ywb/init.dart @@ -15,8 +15,10 @@ class YwbInit { static void init() { serviceService = Dev.demoMode ? const DemoYwbServiceService() : const YwbServiceService(); - serviceStorage = const YwbServiceStorage(); applicationService = Dev.demoMode ? const DemoYwbApplicationService() : const YwbApplicationService(); + } + static void initStorage() { applicationStorage = YwbApplicationStorage(); + serviceStorage = const YwbServiceStorage(); } } diff --git a/lib/timetable/init.dart b/lib/timetable/init.dart index 47751db2d..f6c3d7566 100644 --- a/lib/timetable/init.dart +++ b/lib/timetable/init.dart @@ -10,6 +10,9 @@ class TimetableInit { static void init() { service = Dev.demoMode ? const DemoTimetableService() : const TimetableService(); + } + + static void initStorage() { storage = TimetableStorage(); } } diff --git a/lib/update/init.dart b/lib/update/init.dart index 471cbbbfb..70504b766 100644 --- a/lib/update/init.dart +++ b/lib/update/init.dart @@ -6,4 +6,7 @@ class UpdateInit { static void init() { service = const UpdateService(); } + + static void initStorage() { + } } From 6fd8e0aa89455aceeb560d0dfa3187b93994e65e Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 12:31:00 +0800 Subject: [PATCH 076/458] [expense] [statistics] fixed l10n. fixed error in day view with a right now record --- assets/l10n/en.yaml | 2 +- assets/l10n/zh-Hant.yaml | 2 +- lib/init.dart | 3 +- .../widget/chart/delegate.dart | 33 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 812ead088..e44d827fb 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -445,7 +445,7 @@ expenseRecords: maxSpendOf: "with a max spend of {amount}" averageLineLabel: avg hourlyAverage: Hourly average - dailyAverage: daily average + dailyAverage: Daily average monthlyAverage: Monthly average today: Today yesterday: Yesterday diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index a84d74a7d..806049877 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -436,7 +436,7 @@ expenseRecords: stats: title: 消費統計 total: 總計 - summery: 摘要 + summary: 摘要 details: 詳細資料 averageSpendIn: "在 {type} 上,平均消費 {amount}" maxSpendOf: "最大消費為 {amount}" diff --git a/lib/init.dart b/lib/init.dart index f514c8895..31ab68b0e 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -107,7 +107,7 @@ class Init { } static Future initModules() async { - debugPrint("Initializing module storage"); + debugPrint("Initializing modules"); CredentialsInit.init(); TimetableInit.init(); if (!kIsWeb) { @@ -127,6 +127,7 @@ class Init { } static Future initStorage() async { + debugPrint("Initializing module storage"); CredentialsInit.initStorage(); TimetableInit.initStorage(); if (!kIsWeb) { diff --git a/lib/life/expense_records/widget/chart/delegate.dart b/lib/life/expense_records/widget/chart/delegate.dart index 1390e346c..8e4f22d1a 100644 --- a/lib/life/expense_records/widget/chart/delegate.dart +++ b/lib/life/expense_records/widget/chart/delegate.dart @@ -38,10 +38,10 @@ class StatisticsDelegate { }); factory StatisticsDelegate.byMode( - StatisticsMode mode, { - required DateTime start, - required List records, - }) { + StatisticsMode mode, { + required DateTime start, + required List records, + }) { switch (mode) { case StatisticsMode.day: return StatisticsDelegate.day(start: start, records: records); @@ -60,17 +60,16 @@ class StatisticsDelegate { }) { final now = DateTime.now(); final data = List.generate( - now.inTheSameDay(start) ? now.hour : 24, - (i) => [], + now.inTheSameDay(start) ? now.hour + 1 : 24, + (i) => [], ); for (final record in records) { // add data at the same weekday. // sunday goes first data[record.timestamp.hour].add(record); } - final (:total,:type2Stats) = statisticsTransactionByType(records); - final dayTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).toList(); + final (:total, :type2Stats) = statisticsTransactionByType(records); + final dayTotals = data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).toList(); return StatisticsDelegate( mode: StatisticsMode.day, start: start, @@ -103,16 +102,16 @@ class StatisticsDelegate { final now = DateTime.now(); final data = List.generate( start.year == now.year && start.week == now.week ? now.calendarOrderWeekday + 1 : 7, - (i) => [], + (i) => [], ); for (final record in records) { // add data at the same weekday. // sunday goes first data[record.timestamp.calendarOrderWeekday].add(record); } - final (:total,:type2Stats) = statisticsTransactionByType(records); + final (:total, :type2Stats) = statisticsTransactionByType(records); final dayTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); return StatisticsDelegate( mode: StatisticsMode.week, start: start, @@ -142,15 +141,15 @@ class StatisticsDelegate { final now = DateTime.now(); final data = List.generate( start.year == now.year && start.month == now.month ? now.day : daysInMonth(year: start.year, month: start.month), - (i) => [], + (i) => [], ); for (final record in records) { // add data on the same day. data[record.timestamp.day - 1].add(record); } - final (:total,:type2Stats) = statisticsTransactionByType(records); + final (:total, :type2Stats) = statisticsTransactionByType(records); final dayTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); final sep = data.length ~/ 5; return StatisticsDelegate( mode: StatisticsMode.month, @@ -188,9 +187,9 @@ class StatisticsDelegate { // add data in the same month. data[record.timestamp.month - 1].add(record); } - final (:total,:type2Stats) = statisticsTransactionByType(records); + final (:total, :type2Stats) = statisticsTransactionByType(records); final monthTotals = - data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); + data.map((monthRecords) => monthRecords.map((r) => r.deltaAmount).sum).where((total) => total > 0).toList(); return StatisticsDelegate( mode: StatisticsMode.year, start: start, From fb1ac727e0fe1905c40daef3f9c7c63b53e91716 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 12:32:54 +0800 Subject: [PATCH 077/458] bump some packages to the latest --- pubspec.lock | 68 ++++++++++++++++++++++++++-------------------------- pubspec.yaml | 12 +++++----- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 09ebd4540..a9bc62e21 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" asn1lib: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: audio_session - sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" + sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e url: "https://pub.dev" source: hosted - version: "0.1.18" + version: "0.1.19" auto_size_text: dependency: "direct main" description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -669,10 +669,10 @@ packages: dependency: transitive description: name: flex_seed_scheme - sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" flutter: dependency: "direct main" description: flutter @@ -783,10 +783,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "592dc01a18961a51c24ae5d963b724b2b7fa4a95c100fe8eb6ca8a5a4732cadf" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" flutter_riverpod: dependency: "direct main" description: @@ -961,10 +961,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: "36524bfb3f0b4ec952c3202466fdd69ad1f7ac1dd9b0a7564177707e45bfaeb9" + sha256: ae30b28cc73053f79fd46b15f430db16cae22a0554e6cd25333c840b310b0270 url: "https://pub.dev" source: hosted - version: "7.6.8" + version: "7.6.9" glob: dependency: transitive description: @@ -977,10 +977,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: f6ba8eed5fa831e461122de577d4a26674a1d836e2956abe6c0f6c4d952e6673 + sha256: "771c8feb40ad0ef639973d7ecf1b43d55ffcedb2207fd43fab030f5639e40446" url: "https://pub.dev" source: hosted - version: "13.2.3" + version: "13.2.4" graphs: dependency: transitive description: @@ -1073,10 +1073,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + sha256: "1f498d086203360cca099d20ffea2963f48c39ce91bdd8a3b6d4a045786b02c8" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.0.8" image_picker_android: dependency: transitive description: @@ -1121,10 +1121,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38" + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.4" + version: "2.10.0" image_picker_windows: dependency: transitive description: @@ -1433,18 +1433,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" path_provider_foundation: dependency: transitive description: @@ -1561,10 +1561,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" pool: dependency: transitive description: @@ -1737,10 +1737,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: @@ -1886,10 +1886,10 @@ packages: dependency: transitive description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" sqflite_common: dependency: transitive description: @@ -2070,10 +2070,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: @@ -2190,10 +2190,10 @@ packages: dependency: transitive description: name: video_player - sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23 + sha256: db6a72d8f4fd155d0189845678f55ad2fd54b02c10dcafd11c068dbb631286c0 url: "https://pub.dev" source: hosted - version: "2.8.3" + version: "2.8.6" video_player_android: dependency: transitive description: @@ -2254,10 +2254,10 @@ packages: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "582f2f7aecc7376332d961a0dd1efa9378ce117657e0ade55d9ff72699a55e82" + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 59da3a73e..fdd49642e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,19 +24,19 @@ dependencies: # Basic logger: ^2.2.0 device_info_plus: ^9.1.2 - path_provider: ^2.1.2 + path_provider: ^2.1.3 version: ^3.0.2 yaml: ^3.1.2 path: ^1.8.3 collection: ^1.18.0 - shared_preferences: ^2.2.2 + shared_preferences: ^2.2.3 flutter_riverpod: ^2.5.1 hive: ^2.2.3 hive_flutter: ^1.1.0 json_annotation: ^4.8.1 copy_with_extension: ^5.0.4 freezed_annotation: ^2.4.1 - get_it: ^7.6.8 + get_it: ^7.6.9 flutter_image_compress: ^2.2.0 # Supporting scrolling screenshot on Android, like MIUI @@ -84,9 +84,9 @@ dependencies: app_links: ^4.0.1 # Open with other APP/programs open_file: ^3.3.2 - url_launcher: ^6.2.5 + url_launcher: ^6.2.6 # Open Android / iOS system image picker - image_picker: ^1.0.7 + image_picker: ^1.0.8 file_picker: ^8.0.0+1 share_plus: ^7.2.2 app_settings: ^5.1.1 @@ -104,7 +104,7 @@ dependencies: add_2_calendar: ^3.0.1 # UI - go_router: ^13.2.3 + go_router: ^13.2.4 fl_chart: ^0.67.0 flutter_svg: ^2.0.10+1 flutter_svg_provider: ^1.0.7 From 84d2493214677563039cd8f95abe8c29a32a9612 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 15:09:45 +0800 Subject: [PATCH 078/458] moved $Key into lifecycle.dart --- lib/app.dart | 1 + lib/game/suika/presenter/dialog_presenter.dart | 2 +- lib/init.dart | 2 +- lib/life/expense_records/page/statistics.dart | 13 ++----------- lib/life/expense_records/widget/chart/delegate.dart | 2 +- lib/life/expense_records/widget/transaction.dart | 2 +- lib/lifecycle.dart | 5 +++++ lib/platform/quick_action.dart | 2 +- lib/r.dart | 2 +- lib/route.dart | 2 +- lib/session/sso.dart | 2 +- lib/utils/date.dart | 2 +- 12 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 lib/lifecycle.dart diff --git a/lib/app.dart b/lib/app.dart index 0d319baec..65d36fb1b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/files.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/qrcode/handle.dart'; import 'package:sit/r.dart'; import 'package:sit/route.dart'; diff --git a/lib/game/suika/presenter/dialog_presenter.dart b/lib/game/suika/presenter/dialog_presenter.dart index a5f285dce..19377cdb8 100644 --- a/lib/game/suika/presenter/dialog_presenter.dart +++ b/lib/game/suika/presenter/dialog_presenter.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:sit/route.dart'; +import 'package:sit/lifecycle.dart'; import '../domain/game_state.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/init.dart b/lib/init.dart index 31ab68b0e..d41dbe2ff 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -4,6 +4,7 @@ import 'package:sit/entity/campus.dart'; import 'package:flutter/foundation.dart'; import 'package:sit/credentials/init.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/session/backend.dart'; import 'package:sit/storage/hive/init.dart'; import 'package:sit/session/class2nd.dart'; @@ -29,7 +30,6 @@ import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:sit/storage/hive/cookie.dart'; import 'package:sit/network/dio.dart'; -import 'package:sit/route.dart'; import 'package:sit/session/sso.dart'; import 'package:sit/update/init.dart'; diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index f167bb91b..c91394f9f 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -37,16 +37,7 @@ final _allRecords = Provider.autoDispose((ref) { }); final _statisticsMode = - NotifierProvider.autoDispose<_StatisticsModeNotifier, StatisticsMode>(_StatisticsModeNotifier.new); - -class _StatisticsModeNotifier extends AutoDisposeNotifier { - @override - StatisticsMode build() => StatisticsMode.week; - - void set(StatisticsMode mode) { - state = mode; - } -} + StateProvider.autoDispose((ref) => StatisticsMode.week); final _startTime2Records = Provider.autoDispose((ref) { final mode = ref.watch(_statisticsMode); @@ -191,7 +182,7 @@ class _ExpenseStatisticsPageState extends ConsumerState { .toList(), selected: {selected}, onSelectionChanged: (newSelection) { - ref.read(_statisticsMode.notifier).set(newSelection.first); + ref.read(_statisticsMode.notifier).state = newSelection.first; }, ); } diff --git a/lib/life/expense_records/widget/chart/delegate.dart b/lib/life/expense_records/widget/chart/delegate.dart index 8e4f22d1a..84ac91e92 100644 --- a/lib/life/expense_records/widget/chart/delegate.dart +++ b/lib/life/expense_records/widget/chart/delegate.dart @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/l10n/time.dart'; import 'package:sit/life/expense_records/utils.dart'; -import 'package:sit/route.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/utils/date.dart'; import 'package:sit/utils/format.dart'; import 'package:statistics/statistics.dart'; diff --git a/lib/life/expense_records/widget/transaction.dart b/lib/life/expense_records/widget/transaction.dart index ec4bf593a..545413c68 100644 --- a/lib/life/expense_records/widget/transaction.dart +++ b/lib/life/expense_records/widget/transaction.dart @@ -18,7 +18,7 @@ class TransactionTile extends StatelessWidget { title: Text(title ?? i18n.unknown, style: context.textTheme.titleSmall), subtitle: [ context.formatYmdhmsNum(transaction.timestamp).text(), - if (title != transaction.note) transaction.note.text(), + if (title != transaction.note && transaction.note.isNotEmpty) transaction.note.text(), if (Dev.on) "${transaction.balanceBefore} => ${transaction.balanceAfter}".text(), ].column(caa: CrossAxisAlignment.start), leading: transaction.type.icon.make(color: transaction.type.color, size: 32), diff --git a/lib/lifecycle.dart b/lib/lifecycle.dart new file mode 100644 index 000000000..e296a0211 --- /dev/null +++ b/lib/lifecycle.dart @@ -0,0 +1,5 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final $Key = GlobalKey(); +final online = StateProvider((ref) => false); diff --git a/lib/platform/quick_action.dart b/lib/platform/quick_action.dart index 8a9c0f3ed..75bae731f 100644 --- a/lib/platform/quick_action.dart +++ b/lib/platform/quick_action.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:quick_actions/quick_actions.dart'; -import 'package:sit/route.dart'; +import 'package:sit/lifecycle.dart'; class _Type { const _Type._(); diff --git a/lib/r.dart b/lib/r.dart index 939fda5da..04b9c383b 100644 --- a/lib/r.dart +++ b/lib/r.dart @@ -32,7 +32,7 @@ class R { static const eduEmailDomain = "mail.sit.edu.cn"; static const demoModeOaCredentials = Credentials(account: "2300421153", password: "liplum-sit-life"); static const iosAppId = "6468989112"; - static const iosAppStoreUrl = "https://apps.apple.com/cn/app/$iosAppId"; + static const iosAppStoreUrl = "https://apps.apple.com/app/$iosAppId"; static String formatEduEmail({required String username}) { return "$username@$eduEmailDomain"; diff --git a/lib/route.dart b/lib/route.dart index d8b036a5a..b937937b7 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -11,6 +11,7 @@ import 'package:sit/game/index.dart'; import 'package:sit/game/minesweeper/index.dart'; import 'package:sit/game/suika/index.dart'; import 'package:sit/index.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/me/edu_email/page/login.dart'; import 'package:sit/me/edu_email/page/outbox.dart'; import 'package:sit/school/class2nd/entity/attended.dart'; @@ -67,7 +68,6 @@ import 'package:sit/timetable/page/p13n/palette.dart'; import 'package:sit/widgets/image.dart'; import 'package:sit/widgets/webview/page.dart'; -final $Key = GlobalKey(); final $TimetableShellKey = GlobalKey(); final $SchoolShellKey = GlobalKey(); final $LifeShellKey = GlobalKey(); diff --git a/lib/session/sso.dart b/lib/session/sso.dart index 9734494b6..fd578bbcc 100644 --- a/lib/session/sso.dart +++ b/lib/session/sso.dart @@ -9,9 +9,9 @@ import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/error.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/init.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/r.dart'; -import 'package:sit/route.dart'; import 'package:sit/session/auth.dart'; import 'package:sit/session/widgets/scope.dart'; import 'package:sit/utils/error.dart'; diff --git a/lib/utils/date.dart b/lib/utils/date.dart index b373f6cd2..6bf2e69b9 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -1,5 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/route.dart'; +import 'package:sit/lifecycle.dart'; bool isLeapYear(int year) { if (year % 400 == 0) return true; From f9b196b8632104433c2bb0f271421520b6eb9ebb Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 15:10:36 +0800 Subject: [PATCH 079/458] format --- lib/app.dart | 4 ++-- lib/game/index.dart | 1 - lib/game/suika/presenter/dialog_presenter.dart | 2 +- lib/init.dart | 2 +- lib/life/electricity/init.dart | 1 + lib/life/expense_records/init.dart | 1 + lib/life/expense_records/page/statistics.dart | 3 +-- lib/life/expense_records/widget/chart/delegate.dart | 2 +- lib/life/expense_records/widget/group.dart | 2 +- lib/lifecycle.dart | 4 ++-- lib/login/init.dart | 4 ++-- lib/me/edu_email/init.dart | 1 + lib/platform/quick_action.dart | 2 +- lib/route.dart | 6 +++--- lib/school/class2nd/init.dart | 1 + lib/school/exam_arrange/init.dart | 1 + lib/school/exam_result/init.dart | 1 + lib/school/index.dart | 1 - lib/school/library/init.dart | 1 + lib/school/oa_announce/init.dart | 1 + lib/school/yellow_pages/init.dart | 3 +-- lib/school/yellow_pages/widgets/list.dart | 2 +- lib/school/ywb/init.dart | 1 + lib/session/sso.dart | 2 +- lib/settings/page/developer.dart | 2 +- lib/settings/page/school.dart | 1 - lib/timetable/widgets/timetable/weekly.dart | 2 +- lib/update/init.dart | 3 +-- lib/utils/date.dart | 2 +- 29 files changed, 31 insertions(+), 28 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 65d36fb1b..67a0b5b00 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -141,7 +141,7 @@ class _PostServiceRunnerState extends State<_PostServiceRunner> { if (!kIsWeb) { Future.delayed(Duration.zero).then((value) async { await checkAppUpdate( - context: $Key.currentContext!, + context: $key.currentContext!, delayAtLeast: const Duration(milliseconds: 3000), manually: false, ); @@ -149,7 +149,7 @@ class _PostServiceRunnerState extends State<_PostServiceRunner> { } WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { $appLink = AppLinks().allUriLinkStream.listen((uri) async { - final navigateCtx = $Key.currentContext; + final navigateCtx = $key.currentContext; if (navigateCtx == null) return; await onHandleQrCodeUriData(context: navigateCtx, qrCodeData: uri); }); diff --git a/lib/game/index.dart b/lib/game/index.dart index 4bf66df51..ff8b05742 100644 --- a/lib/game/index.dart +++ b/lib/game/index.dart @@ -13,7 +13,6 @@ class GamePage extends StatefulWidget { } class _GamePageState extends State { - @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/game/suika/presenter/dialog_presenter.dart b/lib/game/suika/presenter/dialog_presenter.dart index 19377cdb8..8d4963942 100644 --- a/lib/game/suika/presenter/dialog_presenter.dart +++ b/lib/game/suika/presenter/dialog_presenter.dart @@ -6,7 +6,7 @@ import 'package:get_it/get_it.dart'; class DialogPresenter { Future showGameOverDialog(int score) async { return showDialog( - context: $Key.currentContext!, + context: $key.currentContext!, barrierDismissible: false, builder: (context) { return AlertDialog( diff --git a/lib/init.dart b/lib/init.dart index d41dbe2ff..17b2b0a15 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -76,7 +76,7 @@ class Init { debugPrintStack(stackTrace: stackTrace); }, inputCaptcha: (Uint8List imageBytes) async { - final context = $Key.currentContext!; + final context = $key.currentContext!; // return await context.show$Sheet$( // (ctx) => CaptchaSheetPage( // captchaData: imageBytes, diff --git a/lib/life/electricity/init.dart b/lib/life/electricity/init.dart index 80a1ae7d7..8d52c6f03 100644 --- a/lib/life/electricity/init.dart +++ b/lib/life/electricity/init.dart @@ -11,6 +11,7 @@ class ElectricityBalanceInit { static void init() { service = Dev.demoMode ? const DemoElectricityService() : const ElectricityService(); } + static void initStorage() { storage = ElectricityStorage(); } diff --git a/lib/life/expense_records/init.dart b/lib/life/expense_records/init.dart index d393acd08..3136681e6 100644 --- a/lib/life/expense_records/init.dart +++ b/lib/life/expense_records/init.dart @@ -11,6 +11,7 @@ class ExpenseRecordsInit { static void init() { service = Dev.demoMode ? const DemoExpenseService() : const ExpenseService(); } + static void initStorage() { storage = ExpenseStorage(); } diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart index c91394f9f..81851a383 100644 --- a/lib/life/expense_records/page/statistics.dart +++ b/lib/life/expense_records/page/statistics.dart @@ -36,8 +36,7 @@ final _allRecords = Provider.autoDispose((ref) { return all; }); -final _statisticsMode = - StateProvider.autoDispose((ref) => StatisticsMode.week); +final _statisticsMode = StateProvider.autoDispose((ref) => StatisticsMode.week); final _startTime2Records = Provider.autoDispose((ref) { final mode = ref.watch(_statisticsMode); diff --git a/lib/life/expense_records/widget/chart/delegate.dart b/lib/life/expense_records/widget/chart/delegate.dart index 84ac91e92..a9e0c989a 100644 --- a/lib/life/expense_records/widget/chart/delegate.dart +++ b/lib/life/expense_records/widget/chart/delegate.dart @@ -180,7 +180,7 @@ class StatisticsDelegate { required DateTime start, required List records, }) { - final _monthFormat = DateFormat.MMM($Key.currentContext!.locale.toString()); + final _monthFormat = DateFormat.MMM($key.currentContext!.locale.toString()); final now = DateTime.now(); final data = List.generate(start.year == now.year ? now.month : 12, (i) => []); for (final record in records) { diff --git a/lib/life/expense_records/widget/group.dart b/lib/life/expense_records/widget/group.dart index 99b935e8a..b8026049c 100644 --- a/lib/life/expense_records/widget/group.dart +++ b/lib/life/expense_records/widget/group.dart @@ -24,7 +24,7 @@ class TransactionGroupSection extends StatelessWidget { Widget build(BuildContext context) { final (:income, :outcome) = accumulateTransactionIncomeOutcome(records); return GroupedSection( - headerBuilder: (context,expanded, toggleExpand, defaultTrailing) { + headerBuilder: (context, expanded, toggleExpand, defaultTrailing) { return ListTile( title: context.formatYmText((time.toDateTime())).text(), titleTextStyle: context.textTheme.titleMedium, diff --git a/lib/lifecycle.dart b/lib/lifecycle.dart index e296a0211..59893d5f2 100644 --- a/lib/lifecycle.dart +++ b/lib/lifecycle.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -final $Key = GlobalKey(); -final online = StateProvider((ref) => false); +final $key = GlobalKey(); +final $online = StateProvider((ref) => false); diff --git a/lib/login/init.dart b/lib/login/init.dart index df242163d..c7039dec9 100644 --- a/lib/login/init.dart +++ b/lib/login/init.dart @@ -6,6 +6,6 @@ class LoginInit { static void init() { authServerService = const AuthServerService(); } - static void initStorage() { - } + + static void initStorage() {} } diff --git a/lib/me/edu_email/init.dart b/lib/me/edu_email/init.dart index 324874392..5896be969 100644 --- a/lib/me/edu_email/init.dart +++ b/lib/me/edu_email/init.dart @@ -8,6 +8,7 @@ class EduEmailInit { static void init() { service = MailService(); } + static void initStorage() { storage = const EduEmailStorage(); } diff --git a/lib/platform/quick_action.dart b/lib/platform/quick_action.dart index 75bae731f..8635b81be 100644 --- a/lib/platform/quick_action.dart +++ b/lib/platform/quick_action.dart @@ -14,7 +14,7 @@ class QuickAction { static const QuickActions _quickActions = QuickActions(); static void quickActionHandler(String type) { - final ctx = $Key.currentContext; + final ctx = $key.currentContext; if (ctx == null) return; switch (type) { case _Type.examArrange: diff --git a/lib/route.dart b/lib/route.dart index b937937b7..53c4937a4 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -88,7 +88,7 @@ String? _loginRequired(BuildContext ctx, GoRouterState state) { FutureOr _redirectRoot(BuildContext ctx, GoRouterState state) { final loginStatus = ProviderScope.containerOf(ctx).read(CredentialsInit.storage.$oaLoginStatus); - if (loginStatus == LoginStatus.never) { + if (loginStatus == LoginStatus.never) { // allow to access settings page. if (state.matchedLocation.startsWith("/tools")) return null; if (state.matchedLocation.startsWith("/settings")) return null; @@ -186,7 +186,7 @@ final _toolsRoutes = [ ), GoRoute( path: "/tools/scanner", - parentNavigatorKey: $Key, + parentNavigatorKey: $key, builder: (ctx, state) => const ScannerPage(), ), ]; @@ -452,7 +452,7 @@ final _gameRoutes = [ GoRouter buildRouter(ValueNotifier $routingConfig) { return GoRouter.routingConfig( routingConfig: $routingConfig, - navigatorKey: $Key, + navigatorKey: $key, initialLocation: "/", debugLogDiagnostics: kDebugMode, errorBuilder: _onError, diff --git a/lib/school/class2nd/init.dart b/lib/school/class2nd/init.dart index 6ea7ec421..c7971846f 100644 --- a/lib/school/class2nd/init.dart +++ b/lib/school/class2nd/init.dart @@ -21,6 +21,7 @@ class Class2ndInit { activityService = Dev.demoMode ? const DemoClass2ndActivityService() : const Class2ndActivityService(); applicationService = Dev.demoMode ? const DemoClass2ndApplicationService() : const Class2ndApplicationService(); } + static void initStorage() { pointStorage = Class2ndPointsStorage(); activityStorage = const Class2ndActivityStorage(); diff --git a/lib/school/exam_arrange/init.dart b/lib/school/exam_arrange/init.dart index e6a54e7fd..56dfa49fc 100644 --- a/lib/school/exam_arrange/init.dart +++ b/lib/school/exam_arrange/init.dart @@ -11,6 +11,7 @@ class ExamArrangeInit { static void init() { service = Dev.demoMode ? const DemoExamArrangeService() : const ExamArrangeService(); } + static void initStorage() { storage = ExamArrangeStorage(); } diff --git a/lib/school/exam_result/init.dart b/lib/school/exam_result/init.dart index acc96eac8..6a71c232d 100644 --- a/lib/school/exam_result/init.dart +++ b/lib/school/exam_result/init.dart @@ -17,6 +17,7 @@ class ExamResultInit { ugService = Dev.demoMode ? const DemoExamResultUgService() : const ExamResultUgService(); pgService = Dev.demoMode ? const DemoExamResultPgService() : const ExamResultPgService(); } + static void initStorage() { ugStorage = ExamResultUgStorage(); pgStorage = ExamResultPgStorage(); diff --git a/lib/school/index.dart b/lib/school/index.dart index de963f8ce..01523ee4c 100644 --- a/lib/school/index.dart +++ b/lib/school/index.dart @@ -24,7 +24,6 @@ class SchoolPage extends ConsumerStatefulWidget { } class _SchoolPageState extends ConsumerState { - @override Widget build(BuildContext context) { final userType = ref.watch(CredentialsInit.storage.$oaUserType); diff --git a/lib/school/library/init.dart b/lib/school/library/init.dart index f2d10f758..3940df4e0 100644 --- a/lib/school/library/init.dart +++ b/lib/school/library/init.dart @@ -44,6 +44,7 @@ class LibraryInit { hotSearchService = Dev.demoMode ? const DemoLibraryTrendsService() : const LibraryTrendsService(); borrowService = const LibraryBorrowService(); } + static void initStorage() { searchStorage = const LibrarySearchStorage(); bookStorage = const LibraryBookStorage(); diff --git a/lib/school/oa_announce/init.dart b/lib/school/oa_announce/init.dart index d1ad3d513..633e2e57f 100644 --- a/lib/school/oa_announce/init.dart +++ b/lib/school/oa_announce/init.dart @@ -12,6 +12,7 @@ class OaAnnounceInit { static void init() { service = Dev.demoMode ? const DemoOaAnnounceService() : const OaAnnounceService(); } + static void initStorage() { storage = const OaAnnounceStorage(); } diff --git a/lib/school/yellow_pages/init.dart b/lib/school/yellow_pages/init.dart index b4a90aaa4..574c8125c 100644 --- a/lib/school/yellow_pages/init.dart +++ b/lib/school/yellow_pages/init.dart @@ -3,8 +3,7 @@ import 'package:sit/school/yellow_pages/storage/contact.dart'; class YellowPagesInit { static late YellowPagesStorage storage; - static void init() { - } + static void init() {} static void initStorage() { storage = const YellowPagesStorage(); } diff --git a/lib/school/yellow_pages/widgets/list.dart b/lib/school/yellow_pages/widgets/list.dart index f098f5c83..75015517e 100644 --- a/lib/school/yellow_pages/widgets/list.dart +++ b/lib/school/yellow_pages/widgets/list.dart @@ -67,7 +67,7 @@ class _SchoolContactListState extends State { slivers: department2contacts.entries .mapIndexed( (i, entry) => GroupedSection( - headerBuilder: (context,expanded, toggleExpand, defaultTrailing) { + headerBuilder: (context, expanded, toggleExpand, defaultTrailing) { return ListTile( title: entry.key.text(), titleTextStyle: context.textTheme.titleMedium, diff --git a/lib/school/ywb/init.dart b/lib/school/ywb/init.dart index dc1d44d8e..bafb19b91 100644 --- a/lib/school/ywb/init.dart +++ b/lib/school/ywb/init.dart @@ -17,6 +17,7 @@ class YwbInit { serviceService = Dev.demoMode ? const DemoYwbServiceService() : const YwbServiceService(); applicationService = Dev.demoMode ? const DemoYwbApplicationService() : const YwbApplicationService(); } + static void initStorage() { applicationStorage = YwbApplicationStorage(); serviceStorage = const YwbServiceStorage(); diff --git a/lib/session/sso.dart b/lib/session/sso.dart index fd578bbcc..c62d5e9b1 100644 --- a/lib/session/sso.dart +++ b/lib/session/sso.dart @@ -159,7 +159,7 @@ class SsoSession { } void _setOnline(bool isOnline) { - final ctx = $Key.currentContext; + final ctx = $key.currentContext; if (ctx != null && ctx.mounted) { OaOnlineManagerState.of(ctx).isOnline = isOnline; } diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index 1cfb05687..7c1d01aec 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -67,7 +67,7 @@ class _DeveloperOptionsPageState extends ConsumerState { ListTile( leading: const Icon(Icons.adb), title: "Login demo account".text(), - trailing: const Icon(Icons.login), + trailing: const Icon(Icons.login), onTap: () async { Settings.lastSignature ??= "Liplum"; CredentialsInit.storage.oaCredentials = R.demoModeOaCredentials; diff --git a/lib/settings/page/school.dart b/lib/settings/page/school.dart index 6a6a20a29..0456a7ab8 100644 --- a/lib/settings/page/school.dart +++ b/lib/settings/page/school.dart @@ -16,7 +16,6 @@ class SchoolSettingsPage extends ConsumerStatefulWidget { } class _SchoolSettingsPageState extends ConsumerState { - @override Widget build(BuildContext context) { final userType = ref.watch(CredentialsInit.storage.$oaUserType); diff --git a/lib/timetable/widgets/timetable/weekly.dart b/lib/timetable/widgets/timetable/weekly.dart index 394de4d97..e805f4344 100644 --- a/lib/timetable/widgets/timetable/weekly.dart +++ b/lib/timetable/widgets/timetable/weekly.dart @@ -152,7 +152,7 @@ class _TimetableOneWeekCachedState extends State with Au required SitTimetableLessonPart lesson, required SitTimetableEntity timetable, }) { - final inClassNow = lesson.type.startTime.isBefore(now) && lesson.type.endTime.isAfter(now); + final inClassNow = lesson.type.startTime.isBefore(now) && lesson.type.endTime.isAfter(now); final passed = lesson.type.endTime.isBefore(now); Widget cell = InteractiveCourseCell( lesson: lesson, diff --git a/lib/update/init.dart b/lib/update/init.dart index 70504b766..7e39a8431 100644 --- a/lib/update/init.dart +++ b/lib/update/init.dart @@ -7,6 +7,5 @@ class UpdateInit { service = const UpdateService(); } - static void initStorage() { - } + static void initStorage() {} } diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 6bf2e69b9..693a4fc43 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -86,7 +86,7 @@ String formatDateSpan({ required DateTime to, bool showYear = true, }) { - final local = $Key.currentContext?.locale.toString(); + final local = $key.currentContext?.locale.toString(); if (from.inTheSameDay(to)) { final day = DateFormat.yMMMMd(local); return day.format(from); From 047e597f7eb5adc18328fa97c5a5c3cae52c7788 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 16:07:24 +0800 Subject: [PATCH 080/458] OA online state notifier --- lib/app.dart | 9 ++--- lib/lifecycle.dart | 3 +- lib/route.dart | 3 +- lib/session/sso.dart | 4 +-- lib/session/widgets/scope.dart | 62 ---------------------------------- lib/settings/page/index.dart | 7 ++-- lib/utils/riverpod.dart | 12 +++++++ 7 files changed, 25 insertions(+), 75 deletions(-) delete mode 100644 lib/session/widgets/scope.dart create mode 100644 lib/utils/riverpod.dart diff --git a/lib/app.dart b/lib/app.dart index 67a0b5b00..944fcb5b2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -13,7 +13,6 @@ import 'package:sit/lifecycle.dart'; import 'package:sit/qrcode/handle.dart'; import 'package:sit/r.dart'; import 'package:sit/route.dart'; -import 'package:sit/session/widgets/scope.dart'; import 'package:sit/settings/dev.dart'; import 'package:sit/settings/settings.dart'; import 'package:sit/update/utils.dart'; @@ -101,11 +100,9 @@ class _MimirAppState extends ConsumerState { themeMode: ref.watch(Settings.theme.$themeMode), theme: bakeTheme(ThemeData.light()), darkTheme: bakeTheme(ThemeData.dark()), - builder: (ctx, child) => OaOnlineManager( - child: _PostServiceRunner( - key: const ValueKey("Post service runner"), - child: child ?? const SizedBox(), - ), + builder: (ctx, child) => _PostServiceRunner( + key: const ValueKey("Post service runner"), + child: child ?? const SizedBox(), ), scrollBehavior: const MaterialScrollBehavior().copyWith( dragDevices: { diff --git a/lib/lifecycle.dart b/lib/lifecycle.dart index 59893d5f2..dfdb035a7 100644 --- a/lib/lifecycle.dart +++ b/lib/lifecycle.dart @@ -2,4 +2,5 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; final $key = GlobalKey(); -final $online = StateProvider((ref) => false); +final $oaOnline = StateProvider((ref) => false); +final $schoolNetworkAvailable = StateProvider((ref) => false); diff --git a/lib/route.dart b/lib/route.dart index 53c4937a4..6dd957867 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -43,6 +43,7 @@ import 'package:sit/timetable/page/p13n/background.dart'; import 'package:sit/timetable/page/p13n/cell_style.dart'; import 'package:sit/timetable/page/editor.dart'; import 'package:sit/timetable/page/p13n/palette_editor.dart'; +import 'package:sit/utils/riverpod.dart'; import 'package:sit/widgets/not_found.dart'; import 'package:sit/school/oa_announce/entity/announce.dart'; import 'package:sit/school/oa_announce/page/details.dart'; @@ -87,7 +88,7 @@ String? _loginRequired(BuildContext ctx, GoRouterState state) { } FutureOr _redirectRoot(BuildContext ctx, GoRouterState state) { - final loginStatus = ProviderScope.containerOf(ctx).read(CredentialsInit.storage.$oaLoginStatus); + final loginStatus = ctx.riverpod().read(CredentialsInit.storage.$oaLoginStatus); if (loginStatus == LoginStatus.never) { // allow to access settings page. if (state.matchedLocation.startsWith("/tools")) return null; diff --git a/lib/session/sso.dart b/lib/session/sso.dart index c62d5e9b1..21f121dfa 100644 --- a/lib/session/sso.dart +++ b/lib/session/sso.dart @@ -13,8 +13,8 @@ import 'package:sit/lifecycle.dart'; import 'package:sit/r.dart'; import 'package:sit/session/auth.dart'; -import 'package:sit/session/widgets/scope.dart'; import 'package:sit/utils/error.dart'; +import 'package:sit/utils/riverpod.dart'; import 'package:synchronized/synchronized.dart'; import 'package:encrypt/encrypt.dart'; @@ -161,7 +161,7 @@ class SsoSession { void _setOnline(bool isOnline) { final ctx = $key.currentContext; if (ctx != null && ctx.mounted) { - OaOnlineManagerState.of(ctx).isOnline = isOnline; + ctx.riverpod().read($oaOnline.notifier).state = isOnline; } } diff --git a/lib/session/widgets/scope.dart b/lib/session/widgets/scope.dart deleted file mode 100644 index 5a45b5a1a..000000000 --- a/lib/session/widgets/scope.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class OaOnlineManager extends StatefulWidget { - final Widget child; - - const OaOnlineManager({ - super.key, - required this.child, - }); - - @override - State createState() => OaOnlineManagerState(); -} - -class OaOnlineManagerState extends State { - bool _isOnline = false; - - @override - Widget build(BuildContext context) { - return OaOnlineScope( - isOnline: _isOnline, - child: widget.child, - ); - } - - static OaOnlineManagerState of(BuildContext context) { - final OaOnlineManagerState? result = context.findAncestorStateOfType(); - assert(result != null, 'No OaOnlineScope found in context'); - return result!; - } - - bool get isOnline => _isOnline; - - set isOnline(bool newV) { - if (_isOnline != newV) { - setState(() { - _isOnline = newV; - }); - } - } -} - -class OaOnlineScope extends InheritedWidget { - final bool isOnline; - - const OaOnlineScope({ - super.key, - required this.isOnline, - required super.child, - }); - - static OaOnlineScope of(BuildContext context) { - final OaOnlineScope? result = context.dependOnInheritedWidgetOfExactType(); - assert(result != null, 'No OaOnlineScope found in context'); - return result!; - } - - @override - bool updateShouldNotify(OaOnlineScope oldWidget) { - return isOnline != oldWidget.isOnline; - } -} diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index 31bfd0f00..b0beed7c9 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -8,17 +8,18 @@ import 'package:sit/credentials/entity/login_status.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/login/i18n.dart'; import 'package:sit/network/widgets/entry.dart'; import 'package:sit/storage/hive/init.dart'; import 'package:sit/init.dart'; import 'package:sit/l10n/extension.dart'; -import 'package:sit/session/widgets/scope.dart'; import 'package:sit/settings/settings.dart'; import 'package:sit/school/widgets/campus.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/settings/dev.dart'; import 'package:locale_names/locale_names.dart'; +import 'package:sit/utils/riverpod.dart'; import '../i18n.dart'; import '../../design/widgets/navigation.dart'; @@ -219,11 +220,11 @@ Future _onWipeData(BuildContext context) async { destructive: true, ); if (confirm == true) { - await HiveInit.clear(); // 清除存储 + await HiveInit.clear(); // Clear storage await Init.initNetwork(); await Init.initModules(); if (!context.mounted) return; - OaOnlineManagerState.of(context).isOnline = false; + context.riverpod().read($oaOnline.notifier).state = false; _gotoLogin(context); } } diff --git a/lib/utils/riverpod.dart b/lib/utils/riverpod.dart new file mode 100644 index 000000000..e37790756 --- /dev/null +++ b/lib/utils/riverpod.dart @@ -0,0 +1,12 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +extension BuildContextRiverpodX on BuildContext { + ProviderContainer riverpod({ + bool listen = true, + }) => + ProviderScope.containerOf( + this, + listen: listen, + ); +} From 54bfb0d51c8c53fb8b167c626647af7d4fb3e181 Mon Sep 17 00:00:00 2001 From: Liplum Date: Fri, 12 Apr 2024 16:44:45 +0800 Subject: [PATCH 081/458] [timetable] sort timetable by last modified time --- lib/timetable/entity/platte.dart | 11 ++++++----- lib/timetable/entity/platte.g.dart | 4 ++-- lib/timetable/entity/timetable.dart | 10 +++++----- lib/timetable/entity/timetable.g.dart | 14 +++++++------- lib/timetable/page/editor.dart | 1 + lib/timetable/page/mine.dart | 1 + lib/timetable/page/p13n/palette.dart | 12 +----------- lib/timetable/service/school.demo.dart | 2 +- lib/timetable/utils.dart | 4 ++-- 9 files changed, 26 insertions(+), 33 deletions(-) diff --git a/lib/timetable/entity/platte.dart b/lib/timetable/entity/platte.dart index 7d66ffde1..12cd8172f 100644 --- a/lib/timetable/entity/platte.dart +++ b/lib/timetable/entity/platte.dart @@ -38,6 +38,8 @@ List _colorsToJson(List colors) { return colors.map((entry) => _color2ModeToJson(entry)).toList(); } +DateTime _kLastModified() => DateTime.now(); + @JsonSerializable() class TimetablePalette { @JsonKey() @@ -46,8 +48,8 @@ class TimetablePalette { final String author; @JsonKey(fromJson: _colorsFromJson, toJson: _colorsToJson) final List colors; - @JsonKey() - final DateTime? lastModified; + @JsonKey(defaultValue: _kLastModified) + final DateTime lastModified; static const defaultColor = (light: Colors.white30, dark: Colors.black12); @@ -55,7 +57,7 @@ class TimetablePalette { required this.name, required this.author, required this.colors, - this.lastModified, + required this.lastModified, }); factory TimetablePalette.fromJson(Map json) => _$TimetablePaletteFromJson(json); @@ -137,9 +139,8 @@ class BuiltinTimetablePalette implements TimetablePalette { String get author => authorOverride ?? "timetable.p13n.builtinPalette.$key.author".tr(); @override final List colors; - @override - DateTime? get lastModified => null; + DateTime get lastModified => DateTime.now(); const BuiltinTimetablePalette({ required this.key, diff --git a/lib/timetable/entity/platte.g.dart b/lib/timetable/entity/platte.g.dart index b971b7332..f3226a854 100644 --- a/lib/timetable/entity/platte.g.dart +++ b/lib/timetable/entity/platte.g.dart @@ -10,12 +10,12 @@ TimetablePalette _$TimetablePaletteFromJson(Map json) => Timeta name: json['name'] as String, author: json['author'] as String, colors: _colorsFromJson(json['colors'] as List), - lastModified: json['lastModified'] == null ? null : DateTime.parse(json['lastModified'] as String), + lastModified: json['lastModified'] == null ? _kLastModified() : DateTime.parse(json['lastModified'] as String), ); Map _$TimetablePaletteToJson(TimetablePalette instance) => { 'name': instance.name, 'author': instance.author, 'colors': _colorsToJson(instance.colors), - 'lastModified': instance.lastModified?.toIso8601String(), + 'lastModified': instance.lastModified.toIso8601String(), }; diff --git a/lib/timetable/entity/timetable.dart b/lib/timetable/entity/timetable.dart index 59cf084a9..ac8ce1da1 100644 --- a/lib/timetable/entity/timetable.dart +++ b/lib/timetable/entity/timetable.dart @@ -12,7 +12,7 @@ import '../utils.dart'; part 'timetable.g.dart'; -DateTime _kLastUpdate() => DateTime.now(); +DateTime _kLastModified() => DateTime.now(); @JsonSerializable() @CopyWith(skipFields: true) @@ -34,8 +34,8 @@ class SitTimetable { @JsonKey() final Map courses; - @JsonKey(defaultValue: _kLastUpdate) - final DateTime lastUpdate; + @JsonKey(defaultValue: _kLastModified) + final DateTime lastModified; @JsonKey() final int version; @@ -47,7 +47,7 @@ class SitTimetable { required this.startDate, required this.schoolYear, required this.semester, - required this.lastUpdate, + required this.lastModified, this.signature = "", this.version = 1, }); @@ -61,7 +61,7 @@ class SitTimetable { "startDate": startDate, "schoolYear": schoolYear, "semester": semester, - "lastUpdate": lastUpdate, + "lastModified": lastModified, "signature": signature, }.toString(); } diff --git a/lib/timetable/entity/timetable.g.dart b/lib/timetable/entity/timetable.g.dart index 9e1876d74..b3f1e196a 100644 --- a/lib/timetable/entity/timetable.g.dart +++ b/lib/timetable/entity/timetable.g.dart @@ -20,7 +20,7 @@ abstract class _$SitTimetableCWProxy { DateTime? startDate, int? schoolYear, Semester? semester, - DateTime? lastUpdate, + DateTime? lastModified, String? signature, int? version, }); @@ -47,7 +47,7 @@ class _$SitTimetableCWProxyImpl implements _$SitTimetableCWProxy { Object? startDate = const $CopyWithPlaceholder(), Object? schoolYear = const $CopyWithPlaceholder(), Object? semester = const $CopyWithPlaceholder(), - Object? lastUpdate = const $CopyWithPlaceholder(), + Object? lastModified = const $CopyWithPlaceholder(), Object? signature = const $CopyWithPlaceholder(), Object? version = const $CopyWithPlaceholder(), }) { @@ -76,10 +76,10 @@ class _$SitTimetableCWProxyImpl implements _$SitTimetableCWProxy { ? _value.semester // ignore: cast_nullable_to_non_nullable : semester as Semester, - lastUpdate: lastUpdate == const $CopyWithPlaceholder() || lastUpdate == null - ? _value.lastUpdate + lastModified: lastModified == const $CopyWithPlaceholder() || lastModified == null + ? _value.lastModified // ignore: cast_nullable_to_non_nullable - : lastUpdate as DateTime, + : lastModified as DateTime, signature: signature == const $CopyWithPlaceholder() || signature == null ? _value.signature // ignore: cast_nullable_to_non_nullable @@ -265,7 +265,7 @@ SitTimetable _$SitTimetableFromJson(Map json) => SitTimetable( startDate: DateTime.parse(json['startDate'] as String), schoolYear: json['schoolYear'] as int, semester: $enumDecode(_$SemesterEnumMap, json['semester']), - lastUpdate: json['lastUpdate'] == null ? _kLastUpdate() : DateTime.parse(json['lastUpdate'] as String), + lastModified: json['lastModified'] == null ? _kLastModified() : DateTime.parse(json['lastModified'] as String), signature: json['signature'] as String? ?? "", version: json['version'] as int? ?? 1, ); @@ -278,7 +278,7 @@ Map _$SitTimetableToJson(SitTimetable instance) => { startDate: $selectedDate.value, courses: courses, lastCourseKey: lastCourseKey, + lastModified: DateTime.now(), ); } diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart index 1c88eacb1..ef2218c92 100644 --- a/lib/timetable/page/mine.dart +++ b/lib/timetable/page/mine.dart @@ -93,6 +93,7 @@ class _MyTimetableListPageState extends State { @override Widget build(BuildContext context) { final timetables = TimetableInit.storage.timetable.getRows(); + timetables.sort((a, b) => b.row.lastModified.compareTo(a.row.lastModified)); final selectedId = TimetableInit.storage.timetable.selectedId; final actions = [ if (Settings.focusTimetable) diff --git a/lib/timetable/page/p13n/palette.dart b/lib/timetable/page/p13n/palette.dart index 67061ec1d..d39621c0f 100644 --- a/lib/timetable/page/p13n/palette.dart +++ b/lib/timetable/page/p13n/palette.dart @@ -133,17 +133,7 @@ class _TimetableP13nPageState extends State with SingleTicker Widget buildPaletteList(List<({int id, TimetablePalette row})> palettes) { final selectedId = TimetableInit.storage.palette.selectedId ?? BuiltinTimetablePalettes.classic.id; - palettes.sort((a, b) { - final $a = a.row.lastModified; - final $b = b.row.lastModified; - if ($a == $b) return 0; - if ($a == null) { - return 1; - } else if ($b == null) { - return -1; - } - return $b.compareTo($a); - }); + palettes.sort((a, b) => b.row.lastModified.compareTo(a.row.lastModified)); return CustomScrollView( slivers: [ SliverList.builder( diff --git a/lib/timetable/service/school.demo.dart b/lib/timetable/service/school.demo.dart index b2359c340..a09fb3062 100644 --- a/lib/timetable/service/school.demo.dart +++ b/lib/timetable/service/school.demo.dart @@ -127,7 +127,7 @@ class DemoTimetableService implements TimetableService { startDate: DateTime.now(), schoolYear: info.exactYear, semester: info.semester, - lastUpdate: DateTime.now(), + lastModified: DateTime.now(), ); } diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart index c439f93e8..711898f14 100644 --- a/lib/timetable/utils.dart +++ b/lib/timetable/utils.dart @@ -132,7 +132,7 @@ SitTimetable parseUndergraduateTimetableFromCourseRaw(List Date: Fri, 12 Apr 2024 18:23:36 +0800 Subject: [PATCH 082/458] [test] Test duplicate names --- lib/utils/format.dart | 1 + test/duplicate.dart | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 test/duplicate.dart diff --git a/lib/utils/format.dart b/lib/utils/format.dart index 76309e072..abd80a410 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -20,6 +20,7 @@ String formatWithoutTrailingZeros(double amount) { final _trailingIntRe = RegExp(r"(.*\s+)(\d+)$"); String getDuplicateFileName(String origin, {List? all}) { + assert(all == null || all.contains(origin)); if (all == null || all.isEmpty) { return "$origin 2"; } diff --git a/test/duplicate.dart b/test/duplicate.dart new file mode 100644 index 000000000..83c873654 --- /dev/null +++ b/test/duplicate.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sit/utils/format.dart'; + +void main() { + group("Test duplicate names", () { + test("only one", () { + assert("Test 2" == getDuplicateFileName("Test", all: ["Test"])); + }); + test("two or more without space", () { + assert("TestB 2" == getDuplicateFileName("TestB", all: ["TestA", "TestB"])); + }); + test("two or more with space", () { + assert("Test B 2" == getDuplicateFileName("Test B", all: ["Test A", "Test B"])); + }); + test("two or more", () { + assert("Test B 2" == getDuplicateFileName("Test B", all: ["Test A", "Test B"])); + }); + }); +} From 5044c7a7b768ec0284aad242a22ebe2390e32e61 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sat, 13 Apr 2024 01:54:01 +0800 Subject: [PATCH 083/458] fixed duplicate name issues --- lib/utils/format.dart | 31 ++++++++++++++++++------------- test/duplicate.dart | 20 ++++++++++++++------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/utils/format.dart b/lib/utils/format.dart index abd80a410..1d33d1538 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -1,4 +1,4 @@ -import 'package:sit/utils/collection.dart'; +import 'package:collection/collection.dart'; String formatWithoutTrailingZeros(double amount) { if (amount == 0) return "0"; @@ -21,22 +21,27 @@ final _trailingIntRe = RegExp(r"(.*\s+)(\d+)$"); String getDuplicateFileName(String origin, {List? all}) { assert(all == null || all.contains(origin)); - if (all == null || all.isEmpty) { + if (all == null || all.length <= 1) { return "$origin 2"; } - - final nameHasLargestNumber = all - .map((s) => _extractTrailingNumber(s)) - .map((p) { - final number = p.number; - return number == null ? null : (name: p.name, number: number); - }) - .nonNulls - .maxByOrNull((p) => p.number); - if (nameHasLargestNumber == null) { + final (name: originName, number: originNumber) = _extractTrailingNumber(origin); + final numbers = []; + for (final file in all) { + final (:name, :number) = _extractTrailingNumber(file); + if (number == null) continue; + if (file == origin || name == "$originName ") { + numbers.add(number); + } + } + final maxNumber = numbers.maxOrNull; + if (maxNumber == null) { return "$origin 2"; } - return "${nameHasLargestNumber.name}${nameHasLargestNumber.number + 1}"; + if (originNumber == null) { + return "$originName ${maxNumber + 1}"; + } else { + return "$originName${maxNumber + 1}"; + } } ({String name, int? number}) _extractTrailingNumber(String s) { diff --git a/test/duplicate.dart b/test/duplicate.dart index 83c873654..4cb3713cc 100644 --- a/test/duplicate.dart +++ b/test/duplicate.dart @@ -2,18 +2,26 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sit/utils/format.dart'; void main() { - group("Test duplicate names", () { - test("only one", () { + group("One file", () { + test("duplicate", () { assert("Test 2" == getDuplicateFileName("Test", all: ["Test"])); }); - test("two or more without space", () { + }); + group("files", () { + test("Without space", () { assert("TestB 2" == getDuplicateFileName("TestB", all: ["TestA", "TestB"])); }); - test("two or more with space", () { + test("With space", () { assert("Test B 2" == getDuplicateFileName("Test B", all: ["Test A", "Test B"])); }); - test("two or more", () { - assert("Test B 2" == getDuplicateFileName("Test B", all: ["Test A", "Test B"])); + test("Duplicate again", () { + assert("Test B 3" == getDuplicateFileName("Test B", all: ["Test A", "Test B", "Test B 2"])); + }); + test("Duplicate new one again", () { + assert("Test B 3" == getDuplicateFileName("Test B 2", all: ["Test A", "Test B", "Test B 2"])); + }); + test("Duplicate another", () { + assert("Test A 2" == getDuplicateFileName("Test A", all: ["Test A", "Test B", "Test B 2"])); }); }); } From 22a44ffa839ee4ec54351a28ca563a27c0917681 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sat, 13 Apr 2024 02:02:15 +0800 Subject: [PATCH 084/458] fixed duplicate name issues truly --- lib/utils/format.dart | 2 +- test/duplicate.dart | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/utils/format.dart b/lib/utils/format.dart index 1d33d1538..dbb60be7c 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -29,7 +29,7 @@ String getDuplicateFileName(String origin, {List? all}) { for (final file in all) { final (:name, :number) = _extractTrailingNumber(file); if (number == null) continue; - if (file == origin || name == "$originName ") { + if (file == origin || (originNumber == null && name == "$originName ") || name == originName) { numbers.add(number); } } diff --git a/test/duplicate.dart b/test/duplicate.dart index 4cb3713cc..274173481 100644 --- a/test/duplicate.dart +++ b/test/duplicate.dart @@ -3,17 +3,19 @@ import 'package:sit/utils/format.dart'; void main() { group("One file", () { - test("duplicate", () { + test("Duplicate", () { assert("Test 2" == getDuplicateFileName("Test", all: ["Test"])); }); }); - group("files", () { + group("Two or more files, format", () { test("Without space", () { assert("TestB 2" == getDuplicateFileName("TestB", all: ["TestA", "TestB"])); }); test("With space", () { assert("Test B 2" == getDuplicateFileName("Test B", all: ["Test A", "Test B"])); }); + }); + group("Two or more files, already existing", () { test("Duplicate again", () { assert("Test B 3" == getDuplicateFileName("Test B", all: ["Test A", "Test B", "Test B 2"])); }); @@ -23,5 +25,8 @@ void main() { test("Duplicate another", () { assert("Test A 2" == getDuplicateFileName("Test A", all: ["Test A", "Test B", "Test B 2"])); }); + test("Already have 3, but dupplicate 2", () { + assert("Test 4" == getDuplicateFileName("Test 2", all: ["Test", "Test 2", "Test 3" ])); + }); }); } From 8d18938b3a8a462098db7d72c28bfb824e33ed8c Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 11:30:11 +0800 Subject: [PATCH 085/458] [timetable] allocValidFileName --- lib/timetable/page/mine.dart | 16 +++++++++------- lib/timetable/page/p13n/palette.dart | 6 +++++- lib/utils/format.dart | 6 ++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart index ef2218c92..7aa4d5c93 100644 --- a/lib/timetable/page/mine.dart +++ b/lib/timetable/page/mine.dart @@ -225,11 +225,10 @@ class TimetableCard extends StatelessWidget { label: i18n.delete, icon: context.icons.delete, action: () async { - final confirm = await ctx.showDialogRequest( - title: i18n.mine.deleteRequest, + final confirm = await ctx.showActionRequest( + action: i18n.mine.deleteRequest, desc: i18n.mine.deleteRequestDesc, - yes: i18n.delete, - no: i18n.cancel, + cancel: i18n.cancel, destructive: true, ); if (confirm != true) return; @@ -263,8 +262,12 @@ class TimetableCard extends StatelessWidget { icon: context.icons.edit, activator: const SingleActivator(LogicalKeyboardKey.keyE), action: () async { - final newTimetable = await ctx.push("/timetable/edit/$id"); + var newTimetable = await ctx.push("/timetable/edit/$id"); if (newTimetable != null) { + final newName = allocValidFileName(newTimetable.name); + if (newName != newTimetable.name) { + newTimetable = newTimetable.copyWith(name: newName); + } TimetableInit.storage.timetable[id] = newTimetable; } }, @@ -291,8 +294,7 @@ class TimetableCard extends StatelessWidget { activator: const SingleActivator(LogicalKeyboardKey.keyD), action: () async { final duplicate = timetable.copyWith( - name: getDuplicateFileName(timetable.name, all: allTimetableNames), - ); + name: getDuplicateFileName(timetable.name, all: allTimetableNames), lastModified: DateTime.now()); TimetableInit.storage.timetable.add(duplicate); }, ), diff --git a/lib/timetable/page/p13n/palette.dart b/lib/timetable/page/p13n/palette.dart index d39621c0f..ac9185415 100644 --- a/lib/timetable/page/p13n/palette.dart +++ b/lib/timetable/page/p13n/palette.dart @@ -217,8 +217,12 @@ class PaletteCard extends StatelessWidget { icon: context.icons.edit, activator: const SingleActivator(LogicalKeyboardKey.keyE), action: () async { - final newPalette = await context.push("/timetable/palette/edit/$id"); + var newPalette = await context.push("/timetable/palette/edit/$id"); if (newPalette != null) { + final newName = allocValidFileName(newPalette.name); + if (newName != newPalette.name) { + newPalette = newPalette.copyWith(name: newName); + } TimetableInit.storage.palette[id] = newPalette; } }, diff --git a/lib/utils/format.dart b/lib/utils/format.dart index dbb60be7c..ca1c276da 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -54,3 +54,9 @@ String getDuplicateFileName(String origin, {List? all}) { if (number == null) return (name: prefix, number: null); return (name: prefix, number: number); } + +String allocValidFileName(String name, {List? all}) { + if (all == null || all.isEmpty) return name; + if (!all.contains(name)) return name; + return getDuplicateFileName(name, all: all); +} From f134fa4ad2b1b45e2937cfcd17e889c5682e9491 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 12:56:00 +0800 Subject: [PATCH 086/458] bump dio to 5.4.3 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a9bc62e21..91822ad64 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -437,10 +437,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "0978e9a3e45305a80a7210dbeaf79d6ee8bee33f70c8e542dc654c952070217f" + sha256: "639179e1cc0957779e10dd5b786ce180c477c4c0aca5aaba5d1700fa2e834801" url: "https://pub.dev" source: hosted - version: "5.4.2+1" + version: "5.4.3" dio_cookie_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fdd49642e..66633337b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: html: ^0.15.4 # Dio (http client) - dio: ^5.4.2+1 + dio: ^5.4.3 dio_cookie_manager: ^3.1.1 # WebView and browser related From 4620114dcc08c34f0df1b29222347d5f05e0f1fc Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 13:48:49 +0800 Subject: [PATCH 087/458] fixed edge cases of getDuplicateFileName --- lib/utils/format.dart | 7 ++++--- test/duplicate.dart | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/utils/format.dart b/lib/utils/format.dart index ca1c276da..b5d15a884 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -21,10 +21,11 @@ final _trailingIntRe = RegExp(r"(.*\s+)(\d+)$"); String getDuplicateFileName(String origin, {List? all}) { assert(all == null || all.contains(origin)); - if (all == null || all.length <= 1) { - return "$origin 2"; - } final (name: originName, number: originNumber) = _extractTrailingNumber(origin); + if (originNumber != null && (all == null || all.length <= 1)) { + return "$originName${originNumber + 1}"; + } + if (all == null || all.length <= 1) return "$origin 2"; final numbers = []; for (final file in all) { final (:name, :number) = _extractTrailingNumber(file); diff --git a/test/duplicate.dart b/test/duplicate.dart index 274173481..554d0b6fc 100644 --- a/test/duplicate.dart +++ b/test/duplicate.dart @@ -6,6 +6,10 @@ void main() { test("Duplicate", () { assert("Test 2" == getDuplicateFileName("Test", all: ["Test"])); }); + + test("ending with number", () { + assert("Test 3" == getDuplicateFileName("Test 2", all: ["Test 2"])); + }); }); group("Two or more files, format", () { test("Without space", () { From ce98586ebf3974833ecc3e4ddb59f227f87a4f7a Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 14:06:12 +0800 Subject: [PATCH 088/458] [timetable] removed Card.outlined in palette color box --- lib/timetable/page/p13n/palette.dart | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/timetable/page/p13n/palette.dart b/lib/timetable/page/p13n/palette.dart index ac9185415..61b4664ee 100644 --- a/lib/timetable/page/p13n/palette.dart +++ b/lib/timetable/page/p13n/palette.dart @@ -399,19 +399,15 @@ class PaletteColorsPreview extends StatelessWidget { return colors .map((c) { final color = c.byBrightness(brightness); - return Card.outlined( - color: brightness == Brightness.light ? Colors.black : Colors.white, - margin: EdgeInsets.zero, - child: TweenAnimationBuilder( - tween: ColorTween(begin: color, end: color), - duration: const Duration(milliseconds: 300), - builder: (ctx, value, child) => Card.filled( - margin: EdgeInsets.zero, - color: value, - child: const SizedBox( - width: 32, - height: 32, - ), + return TweenAnimationBuilder( + tween: ColorTween(begin: color, end: color), + duration: const Duration(milliseconds: 300), + builder: (ctx, value, child) => Card.filled( + margin: EdgeInsets.zero, + color: value, + child: const SizedBox( + width: 32, + height: 32, ), ), ); From 1118b57c71bd383458f7f5880a06439496c28fb8 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 16:48:03 +0800 Subject: [PATCH 089/458] [2048] copy with --- lib/game/2048/entity/board.dart | 25 +++------ lib/game/2048/entity/board.g.dart | 75 ++++++++++++++++++++++++++ lib/game/minesweeper/entity/state.dart | 1 + test/duplicate.dart | 2 +- 4 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 lib/game/2048/entity/board.g.dart create mode 100644 lib/game/minesweeper/entity/state.dart diff --git a/lib/game/2048/entity/board.dart b/lib/game/2048/entity/board.dart index 376d53917..036f51d32 100644 --- a/lib/game/2048/entity/board.dart +++ b/lib/game/2048/entity/board.dart @@ -1,8 +1,12 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:uuid/uuid.dart'; import '../entity/tile.dart'; import '../save.dart'; +part "board.g.dart"; + +@CopyWith(skipFields: true) class Board { //Current score on the board final int score; @@ -19,7 +23,7 @@ class Board { //Whether the game is won or not final bool won; - Board({ + const Board({ required this.score, required this.best, required this.tiles, @@ -28,30 +32,13 @@ class Board { }); //Create a model for a new game. - Board.newGame({ + const Board.newGame({ required this.best, required this.tiles, }) : score = 0, over = false, won = false; - //Create an immutable copy of the board - Board copyWith({ - int? score, - int? best, - List? tiles, - bool? over, - bool? won, - }) { - return Board( - score: score ?? this.score, - best: best ?? this.best, - tiles: tiles ?? this.tiles, - over: over ?? this.over, - won: won ?? this.won, - ); - } - // Create a Board from json data factory Board.fromSave(Save2048 save) { final tiles = []; diff --git a/lib/game/2048/entity/board.g.dart b/lib/game/2048/entity/board.g.dart new file mode 100644 index 000000000..9c7f77331 --- /dev/null +++ b/lib/game/2048/entity/board.g.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'board.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$BoardCWProxy { + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// Board(...).copyWith(id: 12, name: "My name") + /// ```` + Board call({ + int? score, + int? best, + List? tiles, + bool? over, + bool? won, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfBoard.copyWith(...)`. +class _$BoardCWProxyImpl implements _$BoardCWProxy { + const _$BoardCWProxyImpl(this._value); + + final Board _value; + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// Board(...).copyWith(id: 12, name: "My name") + /// ```` + Board call({ + Object? score = const $CopyWithPlaceholder(), + Object? best = const $CopyWithPlaceholder(), + Object? tiles = const $CopyWithPlaceholder(), + Object? over = const $CopyWithPlaceholder(), + Object? won = const $CopyWithPlaceholder(), + }) { + return Board( + score: score == const $CopyWithPlaceholder() || score == null + ? _value.score + // ignore: cast_nullable_to_non_nullable + : score as int, + best: best == const $CopyWithPlaceholder() || best == null + ? _value.best + // ignore: cast_nullable_to_non_nullable + : best as int, + tiles: tiles == const $CopyWithPlaceholder() || tiles == null + ? _value.tiles + // ignore: cast_nullable_to_non_nullable + : tiles as List, + over: over == const $CopyWithPlaceholder() || over == null + ? _value.over + // ignore: cast_nullable_to_non_nullable + : over as bool, + won: won == const $CopyWithPlaceholder() || won == null + ? _value.won + // ignore: cast_nullable_to_non_nullable + : won as bool, + ); + } +} + +extension $BoardCopyWith on Board { + /// Returns a callable class that can be used as follows: `instanceOfBoard.copyWith(...)`. + // ignore: library_private_types_in_public_api + _$BoardCWProxy get copyWith => _$BoardCWProxyImpl(this); +} diff --git a/lib/game/minesweeper/entity/state.dart b/lib/game/minesweeper/entity/state.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/lib/game/minesweeper/entity/state.dart @@ -0,0 +1 @@ + diff --git a/test/duplicate.dart b/test/duplicate.dart index 554d0b6fc..ac7fb4f42 100644 --- a/test/duplicate.dart +++ b/test/duplicate.dart @@ -30,7 +30,7 @@ void main() { assert("Test A 2" == getDuplicateFileName("Test A", all: ["Test A", "Test B", "Test B 2"])); }); test("Already have 3, but dupplicate 2", () { - assert("Test 4" == getDuplicateFileName("Test 2", all: ["Test", "Test 2", "Test 3" ])); + assert("Test 4" == getDuplicateFileName("Test 2", all: ["Test", "Test 2", "Test 3"])); }); }); } From d2cb91575f967d9f6a36350cb99e128d9209a656 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 17:56:52 +0800 Subject: [PATCH 090/458] [minesweeper] new GameStates --- lib/game/minesweeper/entity/state.dart | 23 ++++++++ lib/game/minesweeper/entity/state.g.dart | 75 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 lib/game/minesweeper/entity/state.g.dart diff --git a/lib/game/minesweeper/entity/state.dart b/lib/game/minesweeper/entity/state.dart index 8b1378917..2bb628b74 100644 --- a/lib/game/minesweeper/entity/state.dart +++ b/lib/game/minesweeper/entity/state.dart @@ -1 +1,24 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:sit/game/minesweeper/entity/screen.dart'; +import 'board.dart'; +import 'mode.dart'; + +part "state.g.dart"; + +@CopyWith(skipFields: true) +class GameStates { + final bool gameOver; + final bool goodGame; + final GameMode mode; + final Screen screen; + final Board board; + + const GameStates({ + required this.gameOver, + required this.goodGame, + required this.mode, + required this.screen, + required this.board, + }); +} diff --git a/lib/game/minesweeper/entity/state.g.dart b/lib/game/minesweeper/entity/state.g.dart new file mode 100644 index 000000000..baaa9986f --- /dev/null +++ b/lib/game/minesweeper/entity/state.g.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$GameStatesCWProxy { + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// GameStates(...).copyWith(id: 12, name: "My name") + /// ```` + GameStates call({ + bool? gameOver, + bool? goodGame, + GameMode? mode, + Screen? screen, + Board? board, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfGameStates.copyWith(...)`. +class _$GameStatesCWProxyImpl implements _$GameStatesCWProxy { + const _$GameStatesCWProxyImpl(this._value); + + final GameStates _value; + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// GameStates(...).copyWith(id: 12, name: "My name") + /// ```` + GameStates call({ + Object? gameOver = const $CopyWithPlaceholder(), + Object? goodGame = const $CopyWithPlaceholder(), + Object? mode = const $CopyWithPlaceholder(), + Object? screen = const $CopyWithPlaceholder(), + Object? board = const $CopyWithPlaceholder(), + }) { + return GameStates( + gameOver: gameOver == const $CopyWithPlaceholder() || gameOver == null + ? _value.gameOver + // ignore: cast_nullable_to_non_nullable + : gameOver as bool, + goodGame: goodGame == const $CopyWithPlaceholder() || goodGame == null + ? _value.goodGame + // ignore: cast_nullable_to_non_nullable + : goodGame as bool, + mode: mode == const $CopyWithPlaceholder() || mode == null + ? _value.mode + // ignore: cast_nullable_to_non_nullable + : mode as GameMode, + screen: screen == const $CopyWithPlaceholder() || screen == null + ? _value.screen + // ignore: cast_nullable_to_non_nullable + : screen as Screen, + board: board == const $CopyWithPlaceholder() || board == null + ? _value.board + // ignore: cast_nullable_to_non_nullable + : board as Board, + ); + } +} + +extension $GameStatesCopyWith on GameStates { + /// Returns a callable class that can be used as follows: `instanceOfGameStates.copyWith(...)`. + // ignore: library_private_types_in_public_api + _$GameStatesCWProxy get copyWith => _$GameStatesCWProxyImpl(this); +} From bd74cc40444ae2e0d570e7ea34d368f8946516d3 Mon Sep 17 00:00:00 2001 From: Liplum Date: Sun, 14 Apr 2024 23:54:53 +0800 Subject: [PATCH 091/458] [expense] [statistics] fixed `last month` not shown --- lib/life/expense_records/utils.dart | 2 +- test/duplicate.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart index 44940de05..53b5aff15 100644 --- a/lib/life/expense_records/utils.dart +++ b/lib/life/expense_records/utils.dart @@ -179,7 +179,7 @@ String resolveTime4Display({ if (date.month == now.month) { return i18n.stats.thisMonth; } else if (date.month == now.month - 1) { - return i18n.stats.thisMonth; + return i18n.stats.lastMonth; } else { return monthFormat.format(date); } diff --git a/test/duplicate.dart b/test/duplicate.dart index ac7fb4f42..59573f255 100644 --- a/test/duplicate.dart +++ b/test/duplicate.dart @@ -29,7 +29,7 @@ void main() { test("Duplicate another", () { assert("Test A 2" == getDuplicateFileName("Test A", all: ["Test A", "Test B", "Test B 2"])); }); - test("Already have 3, but dupplicate 2", () { + test("Already have 3, but duplicate 2", () { assert("Test 4" == getDuplicateFileName("Test 2", all: ["Test", "Test 2", "Test 3"])); }); }); From 53ee68358c51f1d49187fe9c5d70e0a22b857ca0 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 00:27:46 +0800 Subject: [PATCH 092/458] [2048] saving game only if any operation performed --- lib/game/2048/manager/board.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/game/2048/manager/board.dart b/lib/game/2048/manager/board.dart index 02096f580..005403232 100644 --- a/lib/game/2048/manager/board.dart +++ b/lib/game/2048/manager/board.dart @@ -274,11 +274,18 @@ class BoardManager extends StateNotifier { return false; } + bool canSave() { + if (state.won || state.over) return false; + if (state.tiles.isEmpty) return false; + if (state.tiles.length == 1 && state.tiles.first.value == 2) return false; + return true; + } + Future save() async { - if (state.won || state.over) { - await Save2048.storage.delete(); - } else { + if (canSave()) { await Save2048.storage.save(state.toSave()); + } else { + await Save2048.storage.delete(); } } } From 17cb9ac3d36f11e9946c33b80eb268cfc81fdd00 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 01:02:50 +0800 Subject: [PATCH 093/458] [2048] CopyWith --- lib/game/2048/entity/board.dart | 2 +- lib/game/2048/entity/tile.dart | 15 +++--- lib/game/2048/entity/tile.g.dart | 78 ++++++++++++++++++++++++++++++-- lib/game/2048/manager/board.dart | 2 +- 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/lib/game/2048/entity/board.dart b/lib/game/2048/entity/board.dart index 036f51d32..1b4094728 100644 --- a/lib/game/2048/entity/board.dart +++ b/lib/game/2048/entity/board.dart @@ -45,7 +45,7 @@ class Board { for (var i = 0; i < save.tiles.length; i++) { final score = save.tiles[i]; if (score > 0) { - tiles.add(Tile(const Uuid().v4(), score, i)); + tiles.add(Tile(id: const Uuid().v4(), value: score, index: i)); } } return Board(score: save.score, best: save.score, tiles: tiles); diff --git a/lib/game/2048/entity/tile.dart b/lib/game/2048/entity/tile.dart index 24e470a97..597754d89 100644 --- a/lib/game/2048/entity/tile.dart +++ b/lib/game/2048/entity/tile.dart @@ -1,8 +1,10 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:json_annotation/json_annotation.dart'; part 'tile.g.dart'; @JsonSerializable(anyMap: true) +@CopyWith(skipFields: true) class Tile { //Unique id used as ValueKey for the TileWidget final String id; @@ -19,7 +21,13 @@ class Tile { //Whether the tile was merged with another tile final bool merged; - Tile(this.id, this.value, this.index, {this.nextIndex, this.merged = false}); + const Tile({ + required this.id, + required this.value, + required this.index, + this.nextIndex, + this.merged = false, + }); //Calculate the current top position based on the current index double getTop(double size) { @@ -47,11 +55,6 @@ class Tile { return (i * size) + (12.0 * (i + 1)); } - //Create an immutable copy of the tile - Tile copyWith({String? id, int? value, int? index, int? nextIndex, bool? merged}) => - Tile(id ?? this.id, value ?? this.value, index ?? this.index, - nextIndex: nextIndex ?? this.nextIndex, merged: merged ?? this.merged); - //Create a Tile from json data factory Tile.fromJson(Map json) => _$TileFromJson(json); diff --git a/lib/game/2048/entity/tile.g.dart b/lib/game/2048/entity/tile.g.dart index 4283639fe..80ef5142f 100644 --- a/lib/game/2048/entity/tile.g.dart +++ b/lib/game/2048/entity/tile.g.dart @@ -2,14 +2,86 @@ part of 'tile.dart'; +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$TileCWProxy { + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// Tile(...).copyWith(id: 12, name: "My name") + /// ```` + Tile call({ + String? id, + int? value, + int? index, + int? nextIndex, + bool? merged, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTile.copyWith(...)`. +class _$TileCWProxyImpl implements _$TileCWProxy { + const _$TileCWProxyImpl(this._value); + + final Tile _value; + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// Tile(...).copyWith(id: 12, name: "My name") + /// ```` + Tile call({ + Object? id = const $CopyWithPlaceholder(), + Object? value = const $CopyWithPlaceholder(), + Object? index = const $CopyWithPlaceholder(), + Object? nextIndex = const $CopyWithPlaceholder(), + Object? merged = const $CopyWithPlaceholder(), + }) { + return Tile( + id: id == const $CopyWithPlaceholder() || id == null + ? _value.id + // ignore: cast_nullable_to_non_nullable + : id as String, + value: value == const $CopyWithPlaceholder() || value == null + ? _value.value + // ignore: cast_nullable_to_non_nullable + : value as int, + index: index == const $CopyWithPlaceholder() || index == null + ? _value.index + // ignore: cast_nullable_to_non_nullable + : index as int, + nextIndex: nextIndex == const $CopyWithPlaceholder() + ? _value.nextIndex + // ignore: cast_nullable_to_non_nullable + : nextIndex as int?, + merged: merged == const $CopyWithPlaceholder() || merged == null + ? _value.merged + // ignore: cast_nullable_to_non_nullable + : merged as bool, + ); + } +} + +extension $TileCopyWith on Tile { + /// Returns a callable class that can be used as follows: `instanceOfTile.copyWith(...)`. + // ignore: library_private_types_in_public_api + _$TileCWProxy get copyWith => _$TileCWProxyImpl(this); +} + // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Tile _$TileFromJson(Map json) => Tile( - json['id'] as String, - json['value'] as int, - json['index'] as int, + id: json['id'] as String, + value: json['value'] as int, + index: json['index'] as int, nextIndex: json['nextIndex'] as int?, merged: json['merged'] as bool? ?? false, ); diff --git a/lib/game/2048/manager/board.dart b/lib/game/2048/manager/board.dart index 005403232..b15a08dea 100644 --- a/lib/game/2048/manager/board.dart +++ b/lib/game/2048/manager/board.dart @@ -126,7 +126,7 @@ class BoardManager extends StateNotifier { final candidates = Iterable.generate(16, (i) => i).toList(); candidates.removeWhere((i) => indexes.contains(i)); final index = candidates[rand.nextInt(candidates.length)]; - return Tile(const Uuid().v4(), 2, index); + return Tile(id: const Uuid().v4(), value: 2, index: index); } //Merge tiles From e2d1d5ba61693c6eabf133194d7b8d2e9e9eae60 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 01:30:21 +0800 Subject: [PATCH 094/458] bump some packages to the latest --- pubspec.lock | 8 ++++---- pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 91822ad64..efbc48390 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -437,10 +437,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "639179e1cc0957779e10dd5b786ce180c477c4c0aca5aaba5d1700fa2e834801" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.3" + version: "5.4.3+1" dio_cookie_manager: dependency: "direct main" description: @@ -2334,10 +2334,10 @@ packages: dependency: "direct main" description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 66633337b..a7064ea01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: html: ^0.15.4 # Dio (http client) - dio: ^5.4.3 + dio: ^5.4.3+1 dio_cookie_manager: ^3.1.1 # WebView and browser related @@ -92,7 +92,7 @@ dependencies: app_settings: ^5.1.1 # Desktop support window_manager: ^0.3.8 - win32_registry: ^1.1.2 + win32_registry: ^1.1.3 # Get package info (version) package_info_plus: ^5.0.1 # Check VPN connection status From 89fc8b2c5e6b3a22dea34e037e75a27482c2c551 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 01:30:36 +0800 Subject: [PATCH 095/458] [workflow] Windows --- .github/workflows/build.yml | 8 -------- .github/workflows/windows.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/windows.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4c91da4c..22fa17ce7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,10 +34,6 @@ jobs: channel: stable cache: true - - name: Restore packages - run: | - flutter pub get - - name: Build APK run: | flutter build apk --target-platform android-arm64,android-arm @@ -112,10 +108,6 @@ jobs: channel: stable cache: true - - name: Restore packages - run: | - flutter pub get - - name: Build iOS run: | flutter build ios --release --no-codesign diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 000000000..be3cdd7dc --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,30 @@ +name: Windows +on: workflow_dispatch + +env: + flutter_version: '3.19.5' + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.flutter_version }} + channel: stable + cache: true + + - name: Build Windows + run: | + flutter pub run build_runner build --delete-conflicting-outputs + flutter build windows + + - name: Upload building + uses: actions/upload-artifact@v3 + with: + name: Windows-x86_64.zip + path: build/windows/runner/Release From 001df07cd10df388a0130c7d0512c6218ccf2fdd Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 01:41:51 +0800 Subject: [PATCH 096/458] [workflow] flutter config --no-cli-animations --- .github/workflows/build.yml | 8 ++++++++ .github/workflows/windows.yml | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22fa17ce7..c39fd4ef8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,10 @@ jobs: channel: stable cache: true + - name: Setup Flutter + run: | + flutter config --no-cli-animations + - name: Build APK run: | flutter build apk --target-platform android-arm64,android-arm @@ -108,6 +112,10 @@ jobs: channel: stable cache: true + - name: Setup Flutter + run: | + flutter config --no-cli-animations + - name: Build iOS run: | flutter build ios --release --no-codesign diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index be3cdd7dc..4949482d4 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -18,6 +18,10 @@ jobs: channel: stable cache: true + - name: Setup Flutter + run: | + flutter config --no-cli-animations + - name: Build Windows run: | flutter pub run build_runner build --delete-conflicting-outputs @@ -27,4 +31,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: Windows-x86_64.zip - path: build/windows/runner/Release + path: build\windows\x64\runner\Release From dcf22403283ff97c05c375b6a07013f7b4f43d63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 01:17:23 +0000 Subject: [PATCH 097/458] Bump actions/upload-artifact from 3 to 4 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4949482d4..3b784536b 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -28,7 +28,7 @@ jobs: flutter build windows - name: Upload building - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Windows-x86_64.zip path: build\windows\x64\runner\Release From e1d2d7bb776e5e697551e83a27c22f03f1269387 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 01:17:27 +0000 Subject: [PATCH 098/458] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/web.yml | 2 +- .github/workflows/windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index de119ce24..9b919cdf7 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -11,7 +11,7 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Flutter uses: subosito/flutter-action@v2 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4949482d4..8cda43c87 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -9,7 +9,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Flutter uses: subosito/flutter-action@v2 From d2d594705c5cbf331c0e97660c6b941916463569 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 01:17:31 +0000 Subject: [PATCH 099/458] Bump bluefireteam/flutter-gh-pages from 7 to 8 Bumps [bluefireteam/flutter-gh-pages](https://github.com/bluefireteam/flutter-gh-pages) from 7 to 8. - [Commits](https://github.com/bluefireteam/flutter-gh-pages/compare/v7...v8) --- updated-dependencies: - dependency-name: bluefireteam/flutter-gh-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index de119ce24..ea89f5291 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -20,6 +20,6 @@ jobs: channel: stable cache: true - - uses: bluefireteam/flutter-gh-pages@v7 + - uses: bluefireteam/flutter-gh-pages@v8 with: baseHref: /mimir/ From 33e6c3dfa4c44cfad0872badae8fb1211103fe9a Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 13:03:15 +0800 Subject: [PATCH 100/458] [windows] update copyright --- windows/runner/Runner.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index a756f8dd4..79a810cd0 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "SIT Life" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "life.mysit.SITLife" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 SIT Life(mysit.life). All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 Liplum Dev. All Rights Reserved." "\0" VALUE "OriginalFilename", "SIT_Life.exe" "\0" VALUE "ProductName", "SIT Life" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" From 9855870f2b8b56bc6ed30eea867b771777b487f7 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 13:16:06 +0800 Subject: [PATCH 101/458] using simple icons instead of antd icons --- lib/me/index.dart | 7 +++---- pubspec.lock | 16 ++++++++-------- pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/me/index.dart b/lib/me/index.dart index fe4cddf5d..d8fabe5c5 100644 --- a/lib/me/index.dart +++ b/lib/me/index.dart @@ -1,9 +1,8 @@ -import 'package:antdesign_icons/antdesign_icons.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; +import 'package:simple_icons/simple_icons.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/game/2048/card.dart'; @@ -78,7 +77,7 @@ class _MePageState extends State { Widget buildQQGroupTile() { return ListTile( - leading: const Icon(AntIcons.qqOutlined), + leading: const Icon(SimpleIcons.tencentqq), title: "QQ交流群".text(), subtitle: _qGroupNumber.text(), trailing: PlatformIconButton( @@ -99,7 +98,7 @@ class _MePageState extends State { Widget buildWechatOfficialAccountTile() { return ListTile( - leading: const Icon(AntIcons.wechatOutlined), + leading: const Icon(SimpleIcons.wechat), title: "微信公众号".text(), subtitle: "小应生活".text(), trailing: PlatformIconButton( diff --git a/pubspec.lock b/pubspec.lock index efbc48390..24df02137 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.11" - antdesign_icons: - dependency: "direct main" - description: - name: antdesign_icons - sha256: fc956ce592a48fd999ec623d0580652a5075525fa262878765eef6144891b41b - url: "https://pub.dev" - source: hosted - version: "0.0.3" app_links: dependency: "direct main" description: @@ -1821,6 +1813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + simple_icons: + dependency: "direct main" + description: + name: simple_icons + sha256: "30067d70a9d72923fbc80e142e17fa46085dfa970e66bc4bede3be4819d05901" + url: "https://pub.dev" + source: hosted + version: "10.1.3" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index a7064ea01..937c936a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -123,7 +123,7 @@ dependencies: animations: ^2.0.11 dynamic_color: ^1.7.0 unicons: ^2.1.1 - antdesign_icons: ^0.0.3 + simple_icons: ^10.1.3 sliver_tools: ^0.2.12 flutter_staggered_grid_view: ^0.7.0 flex_color_picker: ^3.4.1 From 0fb15351c7cf3ca9ef970483c12288a4838e8be4 Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 13:22:27 +0800 Subject: [PATCH 102/458] [exam arrange] alert before exam, 30 minutes by default --- lib/school/exam_arrange/widgets/exam.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/school/exam_arrange/widgets/exam.dart b/lib/school/exam_arrange/widgets/exam.dart index 1c62a98f1..c2bb50747 100644 --- a/lib/school/exam_arrange/widgets/exam.dart +++ b/lib/school/exam_arrange/widgets/exam.dart @@ -37,9 +37,7 @@ class ExamCardContent extends StatelessWidget { return FilledButton.icon( icon: const Icon(Icons.calendar_month), onPressed: () async { - await addExamArrangeToCalendar( - exam, - ); + await addExamArrangeToCalendar(exam); }, label: i18n.addCalendarEvent.text(), ); @@ -92,6 +90,8 @@ Future addExamArrangeToCalendar(ExamEntry exam) async { title: i18n.calendarEventTitleOf(exam.courseName), description: "${i18n.seatNumber} ${exam.seatNumber}", location: "${exam.place} #${exam.seatNumber}", + // alert before exam, 30 minutes by default. + iosParams: const IOSParams(reminder: Duration(minutes: 30)), startDate: start, endDate: end, ); From a72a0abedf7472f22bbbe372058e6017b008e1fd Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 13:30:56 +0800 Subject: [PATCH 103/458] bump packages to the latest --- pubspec.lock | 56 ++++++++++++++++++++++++++-------------------------- pubspec.yaml | 10 +++++----- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 24df02137..5c094387e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -261,10 +261,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" + sha256: e53da939709efb9aad0f3d72a69a8d05f889168b7a138af60ce78bab5c94b135 url: "https://pub.dev" source: hosted - version: "1.7.5" + version: "1.8.1" clock: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -413,10 +413,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -1033,10 +1033,10 @@ packages: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -1393,18 +1393,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: "2c582551839386fa7ddbc7770658be7c0f87f388a4bff72066478f597c34d17f" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "7.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" path: dependency: "direct main" description: @@ -1713,10 +1713,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + sha256: fb5319f3aab4c5dda5ebb92dca978179ba21f8c783ee4380910ef4c1c6824f51 url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "8.0.3" share_plus_platform_interface: dependency: transitive description: @@ -1769,10 +1769,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -1950,18 +1950,18 @@ packages: dependency: "direct main" description: name: super_context_menu - sha256: a8e8d813ed109b6c76ea5ed37ed7d4ac371e2989825edb1b214a2296f0951df6 + sha256: "96d76c6a2a054d0d7a771c449bfde772da38b94a30cefdf4880a9371307b2bac" url: "https://pub.dev" source: hosted - version: "0.8.5" + version: "0.8.11" super_native_extensions: dependency: transitive description: name: super_native_extensions - sha256: f96db6b137a0b135e43034289bb55ca6447b65225076036e81f97ebb6381ffeb + sha256: "3fd7096b0293ea40a968ae9294b45710f9a13996e165ff7c588acd52541c487b" url: "https://pub.dev" source: hosted - version: "0.8.5" + version: "0.8.11" synchronized: dependency: "direct main" description: @@ -2118,10 +2118,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -2246,10 +2246,10 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: c8b7cc80f045533b40a0e6c9109905494e3cf32c0fbd5c62616998e0de44003f url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.4" wakelock_plus_platform_interface: dependency: transitive description: @@ -2270,18 +2270,18 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.5" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 937c936a2..7cce4f940 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: # Basic logger: ^2.2.0 - device_info_plus: ^9.1.2 + device_info_plus: ^10.1.0 path_provider: ^2.1.3 version: ^3.0.2 yaml: ^3.1.2 @@ -67,7 +67,7 @@ dependencies: webview_flutter: ^4.7.0 fk_user_agent: ^2.1.0 flutter_widget_from_html: ^0.14.11 - chewie: ^1.7.5 + chewie: ^1.8.1 cookie_jar: ^4.0.8 flutter_html: ^3.0.0-beta.2 @@ -88,13 +88,13 @@ dependencies: # Open Android / iOS system image picker image_picker: ^1.0.8 file_picker: ^8.0.0+1 - share_plus: ^7.2.2 + share_plus: ^8.0.3 app_settings: ^5.1.1 # Desktop support window_manager: ^0.3.8 win32_registry: ^1.1.3 # Get package info (version) - package_info_plus: ^5.0.1 + package_info_plus: ^7.0.0 # Check VPN connection status check_vpn_connection: ^0.0.2 connectivity_plus: ^5.0.2 @@ -137,7 +137,7 @@ dependencies: carousel_slider: ^4.2.1 # iCalendar file generator enough_icalendar: ^0.16.0 - super_context_menu: ^0.8.5 + super_context_menu: ^0.8.11 # Utils # dart.io.Platform API for Web From 799a21e516bd11ee9b6237613119323c2f91b79e Mon Sep 17 00:00:00 2001 From: Liplum Date: Mon, 15 Apr 2024 17:09:50 +0800 Subject: [PATCH 104/458] [network] check campus network and sso network --- lib/network/checker.dart | 103 +++++++++++------------------ lib/network/page/connected.dart | 60 ++++++++--------- lib/network/page/index.dart | 34 +++++----- lib/network/service/network.dart | 25 +++++-- lib/network/service/network.g.dart | 4 +- lib/network/utils.dart | 95 ++++++++++++++++++++++++++ lib/session/jwxt.dart | 19 ++++++ lib/session/sso.dart | 18 ----- lib/timetable/service/school.dart | 2 +- pubspec.lock | 8 +-- pubspec.yaml | 2 +- 11 files changed, 224 insertions(+), 146 deletions(-) create mode 100644 lib/network/utils.dart diff --git a/lib/network/checker.dart b/lib/network/checker.dart index a2de9b79b..ea18f85e9 100644 --- a/lib/network/checker.dart +++ b/lib/network/checker.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:check_vpn_connection/check_vpn_connection.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; @@ -9,12 +7,11 @@ import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/animation/animated.dart'; import 'package:sit/init.dart'; -import 'package:sit/settings/settings.dart'; +import 'package:sit/network/utils.dart'; import 'package:sit/utils/error.dart'; -import 'package:sit/utils/timer.dart'; import 'package:rettulf/rettulf.dart'; -enum ConnectivityStatus { +enum _Status { none, connecting, connected, @@ -44,38 +41,19 @@ class ConnectivityChecker extends StatefulWidget { State createState() => _ConnectivityCheckerState(); } -const _type2Icon = { - ConnectivityResult.bluetooth: Icons.bluetooth, - ConnectivityResult.wifi: Icons.wifi, - ConnectivityResult.ethernet: Icons.lan, - ConnectivityResult.mobile: Icons.signal_cellular_alt, - ConnectivityResult.none: Icons.signal_wifi_statusbar_null_outlined, - ConnectivityResult.vpn: Icons.vpn_key, -}; - -IconData getConnectionTypeIcon(ConnectivityResult? type, {IconData? fallback}) { - return _type2Icon[type] ?? fallback ?? Icons.wifi_find_outlined; -} - class _ConnectivityCheckerState extends State { - ConnectivityStatus status = ConnectivityStatus.none; - ConnectivityResult? connectionType; - late Timer networkChecker; + _Status status = _Status.none; + late StreamSubscription connectivityChecker; + ConnectivityStatus? connectivityStatus; @override void initState() { super.initState(); - networkChecker = runPeriodically(const Duration(milliseconds: 1000), (Timer t) async { - var type = await Connectivity().checkConnectivity(); - if (type == ConnectivityResult.wifi || type == ConnectivityResult.ethernet) { - if (Settings.proxy.anyEnabled || await CheckVpnConnection.isVpnActive()) { - type = ConnectivityResult.vpn; - } - } - if (connectionType != type) { + connectivityChecker = checkConnectivityPeriodic(period: const Duration(milliseconds: 1000)).listen((status) { + if (connectivityStatus != status) { if (!mounted) return; setState(() { - connectionType = type; + connectivityStatus = status; }); } }); @@ -87,6 +65,12 @@ class _ConnectivityCheckerState extends State { } } + @override + void dispose() { + connectivityChecker.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return [ @@ -105,44 +89,43 @@ class _ConnectivityCheckerState extends State { Future startCheck() async { if (!mounted) return; setState(() { - networkChecker.cancel(); - status = ConnectivityStatus.connecting; + status = _Status.connecting; }); try { final connected = await widget.check(); if (!mounted) return; setState(() { if (connected) { - status = ConnectivityStatus.connected; + status = _Status.connected; } else { - status = ConnectivityStatus.disconnected; + status = _Status.disconnected; } }); } catch (error, stackTrace) { debugPrintError(error, stackTrace); if (!mounted) return; setState(() { - status = ConnectivityStatus.disconnected; + status = _Status.disconnected; }); } } Widget buildStatus(BuildContext ctx) { final tip = switch (status) { - ConnectivityStatus.none => widget.initialDesc ?? _i18n.status.none, - ConnectivityStatus.connecting => _i18n.status.connecting, - ConnectivityStatus.connected => _i18n.status.connected, - ConnectivityStatus.disconnected => _i18n.status.disconnected, + _Status.none => widget.initialDesc ?? _i18n.status.none, + _Status.connecting => _i18n.status.connecting, + _Status.connected => _i18n.status.connected, + _Status.disconnected => _i18n.status.disconnected, }; return tip.text(key: ValueKey(status), style: ctx.textTheme.titleLarge, textAlign: TextAlign.center); } Widget buildButton(BuildContext ctx) { final (tip, onTap) = switch (status) { - ConnectivityStatus.none => (_i18n.button.none, startCheck), - ConnectivityStatus.connecting => (_i18n.button.connecting, null), - ConnectivityStatus.connected => (_i18n.button.connected, widget.onConnected), - ConnectivityStatus.disconnected => (_i18n.button.disconnected, startCheck), + _Status.none => (_i18n.button.none, startCheck), + _Status.connecting => (_i18n.button.connecting, null), + _Status.connected => (_i18n.button.connected, widget.onConnected), + _Status.disconnected => (_i18n.button.disconnected, startCheck), }; return PlatformElevatedButton( onPressed: onTap, @@ -155,16 +138,16 @@ class _ConnectivityCheckerState extends State { Widget buildIndicatorArea(BuildContext ctx) { switch (status) { - case ConnectivityStatus.none: - return buildIcon(ctx, getConnectionTypeIcon(connectionType)); - case ConnectivityStatus.connecting: + case _Status.none: + return buildIcon(ctx, getConnectionTypeIcon(connectivityStatus)); + case _Status.connecting: return const CircularProgressIndicator( key: ValueKey("Waiting"), strokeWidth: 14, ).sizedAll(widget.iconSize); - case ConnectivityStatus.connected: + case _Status.connected: return buildIcon(ctx, context.icons.checkMark); - case ConnectivityStatus.disconnected: + case _Status.disconnected: return buildIcon(ctx, Icons.public_off_rounded); } } @@ -180,12 +163,6 @@ class _ConnectivityCheckerState extends State { widget.iconSize, ); } - - @override - void dispose() { - networkChecker.cancel(); - super.dispose(); - } } const _i18n = _NetworkCheckerI18n(); @@ -227,36 +204,36 @@ class TestConnectionTile extends StatefulWidget { } class _TestConnectionTileState extends State { - var testState = ConnectivityStatus.none; + var testState = _Status.none; @override Widget build(BuildContext context) { return ListTile( - enabled: testState != ConnectivityStatus.connecting, + enabled: testState != _Status.connecting, leading: const Icon(Icons.network_check), title: _i18n.testConnection.text(), subtitle: _i18n.testConnectionDesc.text(), trailing: switch (testState) { - ConnectivityStatus.connecting => const CircularProgressIndicator.adaptive(), - ConnectivityStatus.connected => Icon(context.icons.checkMark, color: Colors.green), - ConnectivityStatus.disconnected => Icon(Icons.public_off_rounded, color: context.$red$), + _Status.connecting => const CircularProgressIndicator.adaptive(), + _Status.connected => Icon(context.icons.checkMark, color: Colors.green), + _Status.disconnected => Icon(Icons.public_off_rounded, color: context.$red$), _ => null, }, onTap: () async { setState(() { - testState = ConnectivityStatus.connecting; + testState = _Status.connecting; }); final bool connected; try { - connected = await Init.ssoSession.checkConnectivity(); + connected = await Init.jwxtSession.checkConnectivity(); if (!mounted) return; setState(() { - testState = connected ? ConnectivityStatus.connected : ConnectivityStatus.disconnected; + testState = connected ? _Status.connected : _Status.disconnected; }); } catch (error) { if (!mounted) return; setState(() { - testState = ConnectivityStatus.disconnected; + testState = _Status.disconnected; }); } }, diff --git a/lib/network/page/connected.dart b/lib/network/page/connected.dart index 09d316020..7edb21907 100644 --- a/lib/network/page/connected.dart +++ b/lib/network/page/connected.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'package:check_vpn_connection/check_vpn_connection.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:sit/network/checker.dart'; import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/timer.dart'; import 'package:rettulf/rettulf.dart'; import '../service/network.dart'; +import '../utils.dart'; import '../widgets/status.dart'; import '../i18n.dart'; @@ -20,34 +17,32 @@ class ConnectedInfo extends StatefulWidget { } class _ConnectedInfoState extends State { - ConnectivityResult? connectionType; - late Timer connectionTypeChecker; - late Timer statusChecker; - CampusNetworkStatus? status; + ConnectivityStatus? connectivityStatus; + CampusNetworkStatus? campusNetworkStatus; + late StreamSubscription connectivityChecker; + late StreamSubscription campusNetworkChecker; @override void initState() { super.initState(); - connectionTypeChecker = runPeriodically(const Duration(milliseconds: 500), (Timer t) async { - var type = await Connectivity().checkConnectivity(); - if (type == ConnectivityResult.wifi || type == ConnectivityResult.ethernet) { - if (await CheckVpnConnection.isVpnActive()) { - type = ConnectivityResult.vpn; - } - } - if (connectionType != type) { + connectivityChecker = checkConnectivityPeriodic(period: const Duration(milliseconds: 500)).listen((status) { + if (connectivityStatus != status) { if (!mounted) return; setState(() { - connectionType = type; + connectivityStatus = status; }); } }); - statusChecker = runPeriodically(const Duration(milliseconds: 1000), (Timer t) async { - final status = await Network.checkCampusNetworkStatus(); - if (this.status != status) { + campusNetworkChecker = checkPeriodic( + period: const Duration(milliseconds: 2000), + check: () async { + return await Network.checkCampusNetworkStatus(); + }, + ).listen((status) { + if (campusNetworkStatus != status) { if (!mounted) return; setState(() { - this.status = status; + campusNetworkStatus = status; }); } }); @@ -55,8 +50,8 @@ class _ConnectedInfoState extends State { @override void dispose() { - connectionTypeChecker.cancel(); - statusChecker.cancel(); + connectivityChecker.cancel(); + campusNetworkChecker.cancel(); super.dispose(); } @@ -67,26 +62,27 @@ class _ConnectedInfoState extends State { duration: const Duration(milliseconds: 500), child: [ Icon( - useProxy ? Icons.vpn_key : getConnectionTypeIcon(connectionType), + useProxy ? Icons.vpn_key : getConnectionTypeIcon(connectivityStatus), size: 120, ).expanded(flex: 5), buildTip().expanded(flex: 3), - ].column(caa: CrossAxisAlignment.stretch, key: ValueKey(connectionType)), + ].column(caa: CrossAxisAlignment.stretch, key: ValueKey(connectivityStatus)), ).padAll(10); } Widget buildTip() { final style = context.textTheme.bodyLarge; - final tip = switch (connectionType) { - ConnectivityResult.wifi => i18n.connectedByWlan, - ConnectivityResult.ethernet => i18n.connectedByEthernet, - ConnectivityResult.vpn => i18n.connectedByVpn, - _ => null, - }; + final tip = connectivityStatus?.vpnEnabled == true + ? i18n.connectedByVpn + : switch (connectivityStatus?.type) { + ConnectivityType.wifi => i18n.connectedByWlan, + ConnectivityType.ethernet => i18n.connectedByEthernet, + _ => null, + }; if (tip == null) return const SizedBox(height: 10); return [ tip.text(textAlign: TextAlign.center, style: style), - CampusNetworkStatusInfo(status: status), + CampusNetworkStatusInfo(status: campusNetworkStatus), ].column().padH(20); } } diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 1dac4edc2..1a4a399c9 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:sit/init.dart'; -import 'package:sit/utils/timer.dart'; import 'package:rettulf/rettulf.dart'; +import '../utils.dart'; import 'connected.dart'; import 'disconnected.dart'; @@ -18,30 +18,26 @@ class NetworkToolPage extends StatefulWidget { class _NetworkToolPageState extends State { bool? isConnected; - late Timer connectivityChecker; + late StreamSubscription connectivityChecker; @override void initState() { super.initState(); - // FIXME: Bad practice to use periodically, because the next request will not await the former one. - connectivityChecker = runPeriodically(const Duration(milliseconds: 3000), (Timer t) async { - bool connected; - try { - connected = await Init.ssoSession.checkConnectivity(); - } catch (err) { - connected = false; - } - if (!mounted) return; + connectivityChecker = checkPeriodic( + period: const Duration(milliseconds: 3000), + check: () async { + try { + return await Init.jwxtSession.checkConnectivity(); + } catch (err) { + return false; + } + }, + ).listen((connected) { if (isConnected != connected) { - setState(() => isConnected = connected); + setState(() { + isConnected = connected; + }); } - // if (connected) { - // // if connected, check the connection slowly - // await Future.delayed(const Duration(seconds: 3)); - // } else { - // // if not connected, check the connection frequently - // await Future.delayed(const Duration(seconds: 1)); - // } }); } diff --git a/lib/network/service/network.dart b/lib/network/service/network.dart index e60fa4d7b..c5ccfd5e5 100644 --- a/lib/network/service/network.dart +++ b/lib/network/service/network.dart @@ -9,22 +9,35 @@ bool _toBool(int num) => num != 0; @JsonSerializable(createToJson: false) class CampusNetworkStatus { - // 1:已登录 - // 0:未登录 + /// 1:logged in + /// 0:not logged in @JsonKey(name: "result", fromJson: _toBool) final bool loggedIn; - // 当前的校园网ip + /// Currently assigned IP in the campus network @JsonKey(name: 'v46ip') final String ip; - // 当前登录学号 + /// the student ID currently logged in with @JsonKey(name: "uid") - String? studentId; + final String? studentId; - CampusNetworkStatus(this.loggedIn, this.ip, {this.studentId}); + const CampusNetworkStatus({ + required this.loggedIn, + required this.ip, + this.studentId, + }); factory CampusNetworkStatus.fromJson(Map json) => _$CampusNetworkStatusFromJson(json); + + @override + String toString() { + return { + "loggedIn": loggedIn, + "ip": ip, + "studentId": studentId, + }.toString(); + } } @JsonSerializable(createToJson: false) diff --git a/lib/network/service/network.g.dart b/lib/network/service/network.g.dart index 5b52a1c35..d1bb122a6 100644 --- a/lib/network/service/network.g.dart +++ b/lib/network/service/network.g.dart @@ -7,8 +7,8 @@ part of 'network.dart'; // ************************************************************************** CampusNetworkStatus _$CampusNetworkStatusFromJson(Map json) => CampusNetworkStatus( - _toBool(json['result'] as int), - json['v46ip'] as String, + loggedIn: _toBool(json['result'] as int), + ip: json['v46ip'] as String, studentId: json['uid'] as String?, ); diff --git a/lib/network/utils.dart b/lib/network/utils.dart new file mode 100644 index 000000000..256b88ed2 --- /dev/null +++ b/lib/network/utils.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; + +enum ConnectivityType { + bluetooth, + wifi, + ethernet, + cellular, + other; +} + +class ConnectivityStatus { + final ConnectivityType? type; + final bool vpnEnabled; + + const ConnectivityStatus({ + required this.type, + required this.vpnEnabled, + }); + + const ConnectivityStatus.disconnected({ + required this.vpnEnabled, + }) : type = null; + + const ConnectivityStatus.otherType({ + required this.vpnEnabled, + }) : type = ConnectivityType.other; + + @override + String toString() { + return "$type, vpn:$vpnEnabled"; + } +} + +ConnectivityType? _parseResult(ConnectivityResult? r) { + assert(r != ConnectivityResult.none); + assert(r != ConnectivityResult.vpn); + return switch (r) { + ConnectivityResult.bluetooth => ConnectivityType.bluetooth, + ConnectivityResult.wifi => ConnectivityType.wifi, + ConnectivityResult.ethernet => ConnectivityType.ethernet, + ConnectivityResult.mobile => ConnectivityType.cellular, + ConnectivityResult.none => null, + ConnectivityResult.vpn => null, + ConnectivityResult.other => ConnectivityType.other, + null => null, + }; +} + +Future checkConnectivity() async { + var types = await Connectivity().checkConnectivity(); + if (types.isEmpty || types.first == ConnectivityResult.none) { + return const ConnectivityStatus.disconnected(vpnEnabled: false); + } + var vpnEnabled = types.contains(ConnectivityResult.vpn); + if (types.contains(ConnectivityResult.other)) { + return ConnectivityStatus.otherType(vpnEnabled: vpnEnabled); + } + final type = types.where((t) => t != ConnectivityResult.vpn && t != ConnectivityResult.other).firstOrNull; + return ConnectivityStatus(type: _parseResult(type), vpnEnabled: vpnEnabled); +} + +Stream checkConnectivityPeriodic({ + required Duration period, +}) { + return checkPeriodic(period: period, check: checkConnectivity); +} + +Stream checkPeriodic({ + required Duration period, + required FutureOr Function() check, +}) async* { + while (true) { + final result = await check(); + debugPrint(result.toString()); + yield result; + await Future.delayed(period); + } +} + +const _type2Icon = { + ConnectivityType.bluetooth: Icons.bluetooth, + ConnectivityType.wifi: Icons.wifi, + ConnectivityType.ethernet: Icons.lan, + ConnectivityType.cellular: Icons.signal_cellular_alt, +}; + +IconData getConnectionTypeIcon(ConnectivityStatus? status, {IconData? fallback}) { + if (status == null) return Icons.wifi_find_outlined; + if (status.vpnEnabled) return Icons.vpn_key; + if (status.type == null) return Icons.signal_wifi_statusbar_null_outlined; + return _type2Icon[status.type] ?? fallback ?? Icons.signal_wifi_statusbar_null_outlined; +} diff --git a/lib/session/jwxt.dart b/lib/session/jwxt.dart index 02e518694..407719c5a 100644 --- a/lib/session/jwxt.dart +++ b/lib/session/jwxt.dart @@ -60,4 +60,23 @@ class JwxtSession { } return response; } + + Future checkConnectivity({ + String url = 'http://jwxt.sit.edu.cn/', + }) async { + try { + await request( + url, + options: Options( + method: "GET", + contentType: Headers.formUrlEncodedContentType, + followRedirects: false, + validateStatus: (status) => status! < 400, + ), + ); + return true; + } catch (e) { + return false; + } + } } diff --git a/lib/session/sso.dart b/lib/session/sso.dart index 21f121dfa..d91571d5d 100644 --- a/lib/session/sso.dart +++ b/lib/session/sso.dart @@ -73,24 +73,6 @@ class SsoSession { this.onError, }); - Future checkConnectivity({ - String url = 'http://jwxt.sit.edu.cn/', - }) async { - try { - await request( - url, - options: Options( - method: "GET", - contentType: Headers.formUrlEncodedContentType, - followRedirects: false, - validateStatus: (status) => status! < 400, - ), - ); - return true; - } catch (e) { - return false; - } - } /// - User try to log in actively on a login page. Future loginLocked(Credentials credentials) async { diff --git a/lib/timetable/service/school.dart b/lib/timetable/service/school.dart index e41a0d0cf..79d9dd3de 100644 --- a/lib/timetable/service/school.dart +++ b/lib/timetable/service/school.dart @@ -25,7 +25,7 @@ class TimetableService { const TimetableService(); Future checkConnectivity() { - return _jwxtSession.ssoSession.checkConnectivity(); + return _jwxtSession.checkConnectivity(); } /// 获取本科生课表 diff --git a/pubspec.lock b/pubspec.lock index 5c094387e..737ff5936 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -293,18 +293,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.2" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "2.0.0" convert: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7cce4f940..61d96ebab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -97,7 +97,7 @@ dependencies: package_info_plus: ^7.0.0 # Check VPN connection status check_vpn_connection: ^0.0.2 - connectivity_plus: ^5.0.2 + connectivity_plus: ^6.0.2 vibration: ^1.8.4 # qrcode scanner mobile_scanner: 4.0.1 From ce61a5ca79f21b0c70bf208fb499e6f25073588f Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 02:03:54 +0800 Subject: [PATCH 105/458] [network] new network tool UI --- assets/l10n/en.yaml | 12 +- lib/init.dart | 12 +- lib/network/checker.dart | 2 +- lib/network/i18n.dart | 18 +- lib/network/page/connected.dart | 88 --------- lib/network/page/disconnected.dart | 79 -------- lib/network/page/index.dart | 177 +++++++++++++++--- lib/r.dart | 3 + lib/school/exam_arrange/service/exam.dart | 4 +- lib/school/exam_result/page/evaluation.dart | 2 +- lib/school/exam_result/service/result.pg.dart | 4 +- lib/school/exam_result/service/result.ug.dart | 4 +- .../{gms.dart => pg_registration.dart} | 6 +- .../{jwxt.dart => ug_registration.dart} | 6 +- lib/timetable/service/school.dart | 16 +- 15 files changed, 205 insertions(+), 228 deletions(-) delete mode 100644 lib/network/page/connected.dart delete mode 100644 lib/network/page/disconnected.dart rename lib/session/{gms.dart => pg_registration.dart} (90%) rename lib/session/{jwxt.dart => ug_registration.dart} (93%) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index e44d827fb..57f32309a 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -503,10 +503,14 @@ networkTool: connectionFailedButCampusNetworkConnected: | The campus network has been connected, but school server is still inaccessible. It's probably due to school server downtime for maintenance or even crash, please try again later. - connectedByProxy: Connected to campus network by HTTP proxy - connectedByVpn: Connected to campus network by VPN - connectedByWlan: Connected to campus network by WLAN - connectedByEthernet: Connected to campus network by Ethernet + campusNetworkConnected: Connected to campus network + campusNetworkConnectedByVpn: Connected to campus network by VPN + studentRegAvailable: Student registration system available + studentRegUnavailable: Student registration system unavailable + ugRegAvailableTip: + ugRegUnavailableTip: + pgRegAvailableTip: + pgRegUnavailableTip: login: login: Login forgotPwd: Forgot password? diff --git a/lib/init.dart b/lib/init.dart index 17b2b0a15..e0fe40e06 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -8,7 +8,7 @@ import 'package:sit/lifecycle.dart'; import 'package:sit/session/backend.dart'; import 'package:sit/storage/hive/init.dart'; import 'package:sit/session/class2nd.dart'; -import 'package:sit/session/gms.dart'; +import 'package:sit/session/pg_registration.dart'; import 'package:sit/session/library.dart'; import 'package:sit/session/ywb.dart'; import 'package:sit/life/electricity/init.dart'; @@ -22,7 +22,7 @@ import 'package:sit/school/oa_announce/init.dart'; import 'package:sit/school/class2nd/init.dart'; import 'package:sit/school/exam_result/init.dart'; import 'package:sit/school/yellow_pages/init.dart'; -import 'package:sit/session/jwxt.dart'; +import 'package:sit/session/ug_registration.dart'; import 'package:sit/timetable/init.dart'; import 'dart:async'; @@ -42,8 +42,8 @@ class Init { static late Dio dio; static late BackendSession backend; static late SsoSession ssoSession; - static late JwxtSession jwxtSession; - static late GmsSession gmsSession; + static late UgRegistrationSession ugRegSession; + static late PgRegistrationSession pgRegSession; static late YwbSession ywbSession; static late LibrarySession librarySession; static late Class2ndSession class2ndSession; @@ -89,7 +89,7 @@ class Init { ); }, ); - jwxtSession = JwxtSession( + ugRegSession = UgRegistrationSession( ssoSession: ssoSession, ); ywbSession = YwbSession( @@ -101,7 +101,7 @@ class Init { class2ndSession = Class2ndSession( ssoSession: ssoSession, ); - gmsSession = GmsSession( + pgRegSession = PgRegistrationSession( ssoSession: ssoSession, ); } diff --git a/lib/network/checker.dart b/lib/network/checker.dart index ea18f85e9..3460a0b4e 100644 --- a/lib/network/checker.dart +++ b/lib/network/checker.dart @@ -225,7 +225,7 @@ class _TestConnectionTileState extends State { }); final bool connected; try { - connected = await Init.jwxtSession.checkConnectivity(); + connected = await Init.ugRegSession.checkConnectivity(); if (!mounted) return; setState(() { testState = connected ? _Status.connected : _Status.disconnected; diff --git a/lib/network/i18n.dart b/lib/network/i18n.dart index f1dcc49b3..4ad46e2e2 100644 --- a/lib/network/i18n.dart +++ b/lib/network/i18n.dart @@ -18,19 +18,25 @@ class _I18n with CommonI18nMixin { String get openWlanSettingsBtn => "$ns.openWlanSettingsBtn".tr(); - String get noAccessTip => "$ns.noAccessTip".tr(); - String get connectionFailedError => "$ns.connectionFailedError".tr(); String get connectionFailedButCampusNetworkConnected => "$ns.connectionFailedButCampusNetworkConnected".tr(); - String get connectedByProxy => "$ns.connectedByProxy".tr(); + String get studentRegAvailable => "$ns.studentRegAvailable".tr(); + + String get studentRegUnavailable => "$ns.studentRegUnavailable".tr(); + + String get ugRegAvailableTip => "$ns.ugRegAvailableTip".tr(); + + String get ugRegUnavailableTip => "$ns.ugRegUnavailableTip".tr(); + + String get pgRegAvailableTip => "$ns.pgRegAvailableTip".tr(); - String get connectedByVpn => "$ns.connectedByVpn".tr(); + String get pgRegUnavailableTip => "$ns.pgRegUnavailableTip".tr(); - String get connectedByWlan => "$ns.connectedByWlan".tr(); + String get campusNetworkConnected => "$ns.campusNetworkConnected".tr(); - String get connectedByEthernet => "$ns.connectedByEthernet".tr(); + String get campusNetworkConnectedByVpn => "$ns.campusNetworkConnectedByVpn".tr(); } class _Easyconnect { diff --git a/lib/network/page/connected.dart b/lib/network/page/connected.dart deleted file mode 100644 index 7edb21907..000000000 --- a/lib/network/page/connected.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../service/network.dart'; -import '../utils.dart'; -import '../widgets/status.dart'; -import '../i18n.dart'; - -class ConnectedInfo extends StatefulWidget { - const ConnectedInfo({super.key}); - - @override - State createState() => _ConnectedInfoState(); -} - -class _ConnectedInfoState extends State { - ConnectivityStatus? connectivityStatus; - CampusNetworkStatus? campusNetworkStatus; - late StreamSubscription connectivityChecker; - late StreamSubscription campusNetworkChecker; - - @override - void initState() { - super.initState(); - connectivityChecker = checkConnectivityPeriodic(period: const Duration(milliseconds: 500)).listen((status) { - if (connectivityStatus != status) { - if (!mounted) return; - setState(() { - connectivityStatus = status; - }); - } - }); - campusNetworkChecker = checkPeriodic( - period: const Duration(milliseconds: 2000), - check: () async { - return await Network.checkCampusNetworkStatus(); - }, - ).listen((status) { - if (campusNetworkStatus != status) { - if (!mounted) return; - setState(() { - campusNetworkStatus = status; - }); - } - }); - } - - @override - void dispose() { - connectivityChecker.cancel(); - campusNetworkChecker.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final useProxy = Settings.proxy.anyEnabled; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: [ - Icon( - useProxy ? Icons.vpn_key : getConnectionTypeIcon(connectivityStatus), - size: 120, - ).expanded(flex: 5), - buildTip().expanded(flex: 3), - ].column(caa: CrossAxisAlignment.stretch, key: ValueKey(connectivityStatus)), - ).padAll(10); - } - - Widget buildTip() { - final style = context.textTheme.bodyLarge; - final tip = connectivityStatus?.vpnEnabled == true - ? i18n.connectedByVpn - : switch (connectivityStatus?.type) { - ConnectivityType.wifi => i18n.connectedByWlan, - ConnectivityType.ethernet => i18n.connectedByEthernet, - _ => null, - }; - if (tip == null) return const SizedBox(height: 10); - return [ - tip.text(textAlign: TextAlign.center, style: style), - CampusNetworkStatusInfo(status: campusNetworkStatus), - ].column().padH(20); - } -} diff --git a/lib/network/page/disconnected.dart b/lib/network/page/disconnected.dart deleted file mode 100644 index b1a817ca2..000000000 --- a/lib/network/page/disconnected.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:sit/utils/timer.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../service/network.dart'; -import '../widgets/quick_button.dart'; -import '../widgets/status.dart'; -import '../i18n.dart'; - -class DisconnectedInfo extends StatefulWidget { - const DisconnectedInfo({super.key}); - - @override - State createState() => _DisconnectedInfoState(); -} - -class _DisconnectedInfoState extends State { - CampusNetworkStatus? status; - late Timer statusChecker; - - @override - void initState() { - super.initState(); - statusChecker = runPeriodically(const Duration(milliseconds: 1000), (Timer t) async { - final status = await Network.checkCampusNetworkStatus(); - if (this.status != status) { - if (!mounted) return; - setState(() { - this.status = status; - }); - } - }); - } - - @override - void dispose() { - statusChecker.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return context.isPortrait ? buildPortrait() : buildLandscape(); - } - - Widget buildPortrait() { - return [ - const Icon(Icons.public_off_outlined, size: 120).expanded(), - [ - buildTip(context), - if (status != null) CampusNetworkStatusInfo(status: status), - ].column().expanded(), - const QuickButtons(), - ].column(caa: CrossAxisAlignment.stretch).padAll(10); - } - - Widget buildLandscape() { - return [ - [ - const Icon(Icons.public_off_outlined, size: 120), - const QuickButtons(), - ].column(maa: MainAxisAlignment.spaceEvenly).expanded(), - [ - buildTip(context), - if (status != null) CampusNetworkStatusInfo(status: status), - ].column().padAll(10).scrolled().expanded(), - ].row(); - } - - Widget buildTip(BuildContext context) { - return Text( - status != null ? i18n.connectionFailedButCampusNetworkConnected : i18n.connectionFailedError, - textAlign: TextAlign.start, - style: context.textTheme.bodyLarge, - ).padH(20); - } -} diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 1a4a399c9..20d7c7944 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sit/credentials/entity/user_type.dart'; +import 'package:sit/credentials/init.dart'; import 'package:sit/init.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/network/service/network.dart'; import '../utils.dart'; -import 'connected.dart'; -import 'disconnected.dart'; import '../i18n.dart'; @@ -17,25 +19,52 @@ class NetworkToolPage extends StatefulWidget { } class _NetworkToolPageState extends State { - bool? isConnected; - late StreamSubscription connectivityChecker; + bool? studentRegAvailable; + CampusNetworkStatus? campusNetworkStatus; + ConnectivityStatus? connectivityStatus; + late StreamSubscription studentRegChecker; + late StreamSubscription connectivityChecker; + late StreamSubscription campusNetworkChecker; @override void initState() { super.initState(); - connectivityChecker = checkPeriodic( - period: const Duration(milliseconds: 3000), + connectivityChecker = checkConnectivityPeriodic( + period: const Duration(milliseconds: 100000), + ).listen((status) { + if (connectivityStatus != status) { + if (!mounted) return; + setState(() { + connectivityStatus = status; + }); + } + }); + studentRegChecker = checkPeriodic( + period: const Duration(milliseconds: 500000), check: () async { try { - return await Init.jwxtSession.checkConnectivity(); + return await Init.ugRegSession.checkConnectivity(); } catch (err) { return false; } }, ).listen((connected) { - if (isConnected != connected) { + if (studentRegAvailable != connected) { + setState(() { + studentRegAvailable = connected; + }); + } + }); + campusNetworkChecker = checkPeriodic( + period: const Duration(milliseconds: 300000), + check: () async { + return await Network.checkCampusNetworkStatus(); + }, + ).listen((status) { + if (campusNetworkStatus != status) { + if (!mounted) return; setState(() { - isConnected = connected; + campusNetworkStatus = status; }); } }); @@ -44,27 +73,129 @@ class _NetworkToolPageState extends State { @override void dispose() { connectivityChecker.cancel(); + studentRegChecker.cancel(); + campusNetworkChecker.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: i18n.title.text(), - ), - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: switch (isConnected) { - true => const ConnectedInfo(key: ValueKey("Connected")), - false => const DisconnectedInfo(key: ValueKey("Disconnected")), - null => const SizedBox(key: ValueKey("null")), - }, - ), - bottomNavigationBar: const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), + body: CustomScrollView( + slivers: [ + SliverAppBar.medium( + title: i18n.title.text(), + // bottom: const PreferredSize( + // preferredSize: Size.fromHeight(4), + // child: LinearProgressIndicator(), + // ), + actions: [], + ), + SliverList.list( + children: [ + ConnectivityInfo( + status: connectivityStatus, + ), + CampusNetworkConnectivityInfo( + status: campusNetworkStatus, + useVpn: connectivityStatus?.vpnEnabled == true, + ), + StudentRegConnectivityInfo( + connected: studentRegAvailable, + ), + ], + ) + ], ), ); } } + +class ConnectivityInfo extends StatelessWidget { + final ConnectivityStatus? status; + + const ConnectivityInfo({ + required this.status, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card.outlined( + child: [ + Icon( + status?.vpnEnabled == true ? Icons.vpn_key : getConnectionTypeIcon(status), + size: 120, + ), + ].column(caa: CrossAxisAlignment.center), + ); + } +} + +class StudentRegConnectivityInfo extends ConsumerWidget { + final bool? connected; + + const StudentRegConnectivityInfo({ + super.key, + required this.connected, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userType = ref.watch(CredentialsInit.storage.$oaUserType); + final widgets = []; + final connected = this.connected == true; + widgets.add((connected ? i18n.studentRegAvailable : i18n.studentRegUnavailable).text()); + if (connected) { + if (userType == OaUserType.undergraduate) { + widgets.add(i18n.ugRegAvailableTip.text()); + } else if (userType == OaUserType.postgraduate) { + widgets.add(i18n.pgRegAvailableTip.text()); + } + } else { + if (userType == OaUserType.undergraduate) { + widgets.add(i18n.ugRegUnavailableTip.text()); + } else if (userType == OaUserType.postgraduate) { + widgets.add(i18n.pgRegUnavailableTip.text()); + } + } + return Card.outlined( + child: widgets.column(caa: CrossAxisAlignment.center), + ); + } +} + +class CampusNetworkConnectivityInfo extends StatelessWidget { + final CampusNetworkStatus? status; + final bool useVpn; + + const CampusNetworkConnectivityInfo({ + super.key, + this.status, + required this.useVpn, + }); + + @override + Widget build(BuildContext context) { + return Card.outlined( + child: buildTip(context), + ); + } + + Widget buildTip(BuildContext context) { + final style = context.textTheme.bodyLarge; + final status = this.status; + var ip = i18n.unknown; + var studentId = i18n.unknown; + if (status != null) { + ip = status.ip; + studentId = status.studentId ?? i18n.unknown; + } + return [ + i18n.campusNetworkConnected.text(style: style), + if (useVpn) i18n.campusNetworkConnectedByVpn.text(), + "${i18n.credentials.studentId}: $studentId".text(style: style), + "${i18n.network.ipAddress}: $ip".text(style: style), + ].column(caa: CrossAxisAlignment.center); + } +} diff --git a/lib/r.dart b/lib/r.dart index 04b9c383b..6516d3681 100644 --- a/lib/r.dart +++ b/lib/r.dart @@ -52,6 +52,7 @@ class R { ]; static final jwxtUri = Uri(scheme: "http", host: "jwxt.sit.edu.cn"); + static final gmsUri = Uri(scheme: "http", host: "gms.sit.edu.cn"); static final authServerUri = Uri(scheme: "https", host: "authserver.sit.edu.cn"); static final class2ndUri = Uri(scheme: "http", host: "sc.sit.edu.cn"); static final schoolCardUri = Uri(scheme: "http", host: "card.sit.edu.cn"); @@ -61,6 +62,7 @@ class R { static final sitUriList = [ authServerUri, jwxtUri, + gmsUri, class2ndUri, schoolCardUri, myPortalUri, @@ -68,6 +70,7 @@ class R { ]; static final sitSchoolNetworkUriList = [ jwxtUri, + gmsUri, class2ndUri, schoolCardUri, libraryUri, diff --git a/lib/school/exam_arrange/service/exam.dart b/lib/school/exam_arrange/service/exam.dart index fb4a3fd99..4e86c4998 100644 --- a/lib/school/exam_arrange/service/exam.dart +++ b/lib/school/exam_arrange/service/exam.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; import 'package:sit/init.dart'; -import 'package:sit/session/jwxt.dart'; +import 'package:sit/session/ug_registration.dart'; import '../entity/exam.dart'; import 'package:sit/school/entity/school.dart'; @@ -9,7 +9,7 @@ import 'package:sit/school/entity/school.dart'; class ExamArrangeService { static const _examRoomUrl = 'http://jwxt.sit.edu.cn/jwglxt/kwgl/kscx_cxXsksxxIndex.html'; - JwxtSession get _session => Init.jwxtSession; + UgRegistrationSession get _session => Init.ugRegSession; const ExamArrangeService(); diff --git a/lib/school/exam_result/page/evaluation.dart b/lib/school/exam_result/page/evaluation.dart index 9c35ad1ca..97fdbf20d 100644 --- a/lib/school/exam_result/page/evaluation.dart +++ b/lib/school/exam_result/page/evaluation.dart @@ -61,7 +61,7 @@ class _TeacherEvaluationPageState extends State { Future loadCookies() async { // refresh the cookies - await Init.jwxtSession.request( + await Init.ugRegSession.request( teacherEvaluationUri.toString(), options: Options( method: "GET", diff --git a/lib/school/exam_result/service/result.pg.dart b/lib/school/exam_result/service/result.pg.dart index d664ea8fe..503bff2a5 100644 --- a/lib/school/exam_result/service/result.pg.dart +++ b/lib/school/exam_result/service/result.pg.dart @@ -2,14 +2,14 @@ import 'package:dio/dio.dart'; import 'package:html/parser.dart'; import 'package:sit/init.dart'; import 'package:sit/school/utils.dart'; -import 'package:sit/session/gms.dart'; +import 'package:sit/session/pg_registration.dart'; import '../entity/result.pg.dart'; class ExamResultPgService { static const _postgraduateScoresUrl = "http://gms.sit.edu.cn/epstar/app/template.jsp"; - GmsSession get _gmsSession => Init.gmsSession; + PgRegistrationSession get _gmsSession => Init.pgRegSession; const ExamResultPgService(); diff --git a/lib/school/exam_result/service/result.ug.dart b/lib/school/exam_result/service/result.ug.dart index c29968330..3edac0260 100644 --- a/lib/school/exam_result/service/result.ug.dart +++ b/lib/school/exam_result/service/result.ug.dart @@ -4,7 +4,7 @@ import 'package:sit/design/animation/progress.dart'; import 'package:sit/init.dart'; import 'package:sit/school/entity/school.dart'; -import 'package:sit/session/jwxt.dart'; +import 'package:sit/session/ug_registration.dart'; import '../entity/result.ug.dart'; @@ -27,7 +27,7 @@ class ExamResultUgService { static const _scorePercentageSelector = 'td:nth-child(3)'; static const _scoreValueSelector = 'td:nth-child(5)'; - JwxtSession get _session => Init.jwxtSession; + UgRegistrationSession get _session => Init.ugRegSession; const ExamResultUgService(); diff --git a/lib/session/gms.dart b/lib/session/pg_registration.dart similarity index 90% rename from lib/session/gms.dart rename to lib/session/pg_registration.dart index 22db0bfb9..9d680cc99 100644 --- a/lib/session/gms.dart +++ b/lib/session/pg_registration.dart @@ -3,11 +3,11 @@ import 'package:dio/dio.dart'; import 'package:sit/session/sso.dart'; /// gms.sit.edu.cn -/// for postgraduate -class GmsSession { +/// Student registration system for postgraduate +class PgRegistrationSession { final SsoSession ssoSession; - const GmsSession({required this.ssoSession}); + const PgRegistrationSession({required this.ssoSession}); Future request( String url, { diff --git a/lib/session/jwxt.dart b/lib/session/ug_registration.dart similarity index 93% rename from lib/session/jwxt.dart rename to lib/session/ug_registration.dart index 407719c5a..897cce4f5 100644 --- a/lib/session/jwxt.dart +++ b/lib/session/ug_registration.dart @@ -4,11 +4,11 @@ import 'package:flutter/foundation.dart'; import 'package:sit/session/sso.dart'; /// jwxt.sit.edu.cn -/// for undergraduate -class JwxtSession { +/// Student registration system for undergraduate +class UgRegistrationSession { final SsoSession ssoSession; - const JwxtSession({required this.ssoSession}); + const UgRegistrationSession({required this.ssoSession}); Future refreshCookie() async { await ssoSession.request( diff --git a/lib/timetable/service/school.dart b/lib/timetable/service/school.dart index 79d9dd3de..e954374e6 100644 --- a/lib/timetable/service/school.dart +++ b/lib/timetable/service/school.dart @@ -5,8 +5,8 @@ import 'package:sit/init.dart'; import 'package:sit/school/entity/school.dart'; import 'package:sit/school/exam_result/init.dart'; -import 'package:sit/session/gms.dart'; -import 'package:sit/session/jwxt.dart'; +import 'package:sit/session/pg_registration.dart'; +import 'package:sit/session/ug_registration.dart'; import 'package:sit/settings/settings.dart'; import '../entity/course.dart'; @@ -18,19 +18,19 @@ class TimetableService { static const _postgraduateTimetableUrl = 'http://gms.sit.edu.cn/epstar/yjs/T_PYGL_KWGL_WSXK/T_PYGL_KWGL_WSXK_XSKB_NEW.jsp'; - JwxtSession get _jwxtSession => Init.jwxtSession; + UgRegistrationSession get _ugRegSession => Init.ugRegSession; - GmsSession get _gmsSession => Init.gmsSession; + PgRegistrationSession get _pgRegSession => Init.pgRegSession; const TimetableService(); Future checkConnectivity() { - return _jwxtSession.checkConnectivity(); + return _ugRegSession.checkConnectivity(); } /// 获取本科生课表 Future fetchUgTimetable(SemesterInfo info) async { - final response = await _jwxtSession.request( + final response = await _ugRegSession.request( _undergraduateTimetableUrl, options: Options( method: "POST", @@ -52,7 +52,7 @@ class TimetableService { /// 获取研究生课表 Future fetchPgTimetable(SemesterInfo info) async { - final timetableRes = await _gmsSession.request( + final timetableRes = await _pgRegSession.request( _postgraduateTimetableUrl, options: Options( method: "POST", @@ -82,7 +82,7 @@ class TimetableService { } Future<({DateTime start, DateTime end})?> getUgSemesterSpan() async { - final res = await _jwxtSession.request( + final res = await _ugRegSession.request( "http://jwxt.sit.edu.cn/jwglxt/xtgl/index_cxAreaFive.html", options: Options( method: "POST", From bcb946424e61b368f90d139896a5af1859739105 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 02:42:15 +0800 Subject: [PATCH 106/458] [network] l10n en of network tool --- assets/l10n/en.yaml | 16 +++++-- lib/design/animation/animated.dart | 5 +- lib/network/checker.dart | 4 +- lib/network/page/index.dart | 74 +++++++++++++++++------------- lib/network/utils.dart | 2 +- 5 files changed, 58 insertions(+), 43 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 57f32309a..06894f1a0 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -485,7 +485,7 @@ networkChecker: none: You need the access to school server to continue. Let's check it now. network: error: Network error - ipAddress: IP Address + ipAddress: IP address connectionTimeoutError: Network timeout connectionTimeoutErrorDesc: "This function requires access to the campus network. NOTE: It also occurs when the school server is down for maintenance." openToolBtn: Open network tool @@ -507,10 +507,16 @@ networkTool: campusNetworkConnectedByVpn: Connected to campus network by VPN studentRegAvailable: Student registration system available studentRegUnavailable: Student registration system unavailable - ugRegAvailableTip: - ugRegUnavailableTip: - pgRegAvailableTip: - pgRegUnavailableTip: + ugRegAvailableTip: | + You can import timetables from student registration system, and check your exam arrangements and exam results (including GPA). + ugRegUnavailableTip: | + You can't import timetables from student registration system. The only way to import timetables is to load them from a file on your device. + Your exam arrangements, and exam results (including GPA) can't be updated, but can still be viewed if you have already loaded them once. + pgRegAvailableTip: | + You can import timetables from student registration system, and check your exam results. + pgRegUnavailableTip: | + You can't import timetable from student registration system. The only way to import timetables is to load them from a file on your device. + Your exam results can't be updated, but can still be viewed if you have already loaded them once. login: login: Login forgotPwd: Forgot password? diff --git a/lib/design/animation/animated.dart b/lib/design/animation/animated.dart index bb9876de5..907293857 100644 --- a/lib/design/animation/animated.dart +++ b/lib/design/animation/animated.dart @@ -1,3 +1,4 @@ +import 'package:flutter/animation.dart'; import 'package:flutter/material.dart'; extension AnimatedEx on Widget { @@ -16,10 +17,10 @@ extension AnimatedEx on Widget { Widget animatedSized({ Duration duration = Durations.medium2, Alignment align = Alignment.center, - Curve? curve, + Curve curve = Curves.fastEaseInToSlowEaseOut, }) => AnimatedSize( - curve: curve ?? Curves.linear, + curve: curve, duration: duration, alignment: align, child: this, diff --git a/lib/network/checker.dart b/lib/network/checker.dart index 3460a0b4e..76952c941 100644 --- a/lib/network/checker.dart +++ b/lib/network/checker.dart @@ -75,11 +75,11 @@ class _ConnectivityCheckerState extends State { Widget build(BuildContext context) { return [ AnimatedSize( - duration: const Duration(milliseconds: 300), + duration: Durations.medium2, child: buildIndicatorArea(context).animatedSwitched(), ), AnimatedSize( - duration: const Duration(milliseconds: 300), + duration: Durations.medium2, child: buildStatus(context).animatedSwitched(), ), buildButton(context), diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 20d7c7944..4fb564c3d 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/user_type.dart'; import 'package:sit/credentials/init.dart'; +import 'package:sit/design/animation/animated.dart'; +import 'package:sit/design/widgets/card.dart'; import 'package:sit/init.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/network/service/network.dart'; @@ -30,7 +32,7 @@ class _NetworkToolPageState extends State { void initState() { super.initState(); connectivityChecker = checkConnectivityPeriodic( - period: const Duration(milliseconds: 100000), + period: const Duration(milliseconds: 1000), ).listen((status) { if (connectivityStatus != status) { if (!mounted) return; @@ -40,7 +42,7 @@ class _NetworkToolPageState extends State { } }); studentRegChecker = checkPeriodic( - period: const Duration(milliseconds: 500000), + period: const Duration(milliseconds: 5000), check: () async { try { return await Init.ugRegSession.checkConnectivity(); @@ -56,7 +58,7 @@ class _NetworkToolPageState extends State { } }); campusNetworkChecker = checkPeriodic( - period: const Duration(milliseconds: 300000), + period: const Duration(milliseconds: 3000), check: () async { return await Network.checkCampusNetworkStatus(); }, @@ -95,14 +97,14 @@ class _NetworkToolPageState extends State { children: [ ConnectivityInfo( status: connectivityStatus, - ), + ).padSymmetric(v: 16, h: 8).inOutlinedCard().animatedSized(), CampusNetworkConnectivityInfo( status: campusNetworkStatus, useVpn: connectivityStatus?.vpnEnabled == true, - ), + ).padSymmetric(v: 16, h: 8).inOutlinedCard().animatedSized(), StudentRegConnectivityInfo( connected: studentRegAvailable, - ), + ).padSymmetric(v: 16, h: 8).inOutlinedCard().animatedSized(), ], ) ], @@ -121,14 +123,17 @@ class ConnectivityInfo extends StatelessWidget { @override Widget build(BuildContext context) { - return Card.outlined( - child: [ - Icon( - status?.vpnEnabled == true ? Icons.vpn_key : getConnectionTypeIcon(status), - size: 120, - ), - ].column(caa: CrossAxisAlignment.center), - ); + final status = this.status; + return [ + Icon( + status == null + ? Icons.public_off + : status.vpnEnabled == true + ? Icons.vpn_key + : getConnectionTypeIcon(status), + size: 120, + ), + ].column(caa: CrossAxisAlignment.center); } } @@ -145,23 +150,31 @@ class StudentRegConnectivityInfo extends ConsumerWidget { final userType = ref.watch(CredentialsInit.storage.$oaUserType); final widgets = []; final connected = this.connected == true; - widgets.add((connected ? i18n.studentRegAvailable : i18n.studentRegUnavailable).text()); + final textTheme = context.textTheme; + widgets.add((connected ? i18n.studentRegAvailable : i18n.studentRegUnavailable).text( + style: textTheme.titleMedium, + )); + Widget buildTip(String tip) { + return tip.text( + textAlign: TextAlign.center, + style: textTheme.bodyLarge, + ); + } + if (connected) { if (userType == OaUserType.undergraduate) { - widgets.add(i18n.ugRegAvailableTip.text()); + widgets.add(buildTip(i18n.ugRegAvailableTip)); } else if (userType == OaUserType.postgraduate) { - widgets.add(i18n.pgRegAvailableTip.text()); + widgets.add(buildTip(i18n.pgRegAvailableTip)); } } else { if (userType == OaUserType.undergraduate) { - widgets.add(i18n.ugRegUnavailableTip.text()); + widgets.add(buildTip(i18n.ugRegUnavailableTip)); } else if (userType == OaUserType.postgraduate) { - widgets.add(i18n.pgRegUnavailableTip.text()); + widgets.add(buildTip(i18n.pgRegUnavailableTip)); } } - return Card.outlined( - child: widgets.column(caa: CrossAxisAlignment.center), - ); + return widgets.column(caa: CrossAxisAlignment.center); } } @@ -177,24 +190,19 @@ class CampusNetworkConnectivityInfo extends StatelessWidget { @override Widget build(BuildContext context) { - return Card.outlined( - child: buildTip(context), - ); - } - - Widget buildTip(BuildContext context) { final style = context.textTheme.bodyLarge; final status = this.status; - var ip = i18n.unknown; - var studentId = i18n.unknown; + String? ip = i18n.unknown; + String? studentId; if (status != null) { ip = status.ip; studentId = status.studentId ?? i18n.unknown; } return [ - i18n.campusNetworkConnected.text(style: style), - if (useVpn) i18n.campusNetworkConnectedByVpn.text(), - "${i18n.credentials.studentId}: $studentId".text(style: style), + (useVpn ? i18n.campusNetworkConnectedByVpn : i18n.campusNetworkConnected).text( + style: context.textTheme.titleMedium, + ), + if (studentId != null) "${i18n.credentials.studentId}: $studentId".text(style: style), "${i18n.network.ipAddress}: $ip".text(style: style), ].column(caa: CrossAxisAlignment.center); } diff --git a/lib/network/utils.dart b/lib/network/utils.dart index 256b88ed2..afdfff43c 100644 --- a/lib/network/utils.dart +++ b/lib/network/utils.dart @@ -90,6 +90,6 @@ const _type2Icon = { IconData getConnectionTypeIcon(ConnectivityStatus? status, {IconData? fallback}) { if (status == null) return Icons.wifi_find_outlined; if (status.vpnEnabled) return Icons.vpn_key; - if (status.type == null) return Icons.signal_wifi_statusbar_null_outlined; + if (status.type == null) return Icons.public_off; return _type2Icon[status.type] ?? fallback ?? Icons.signal_wifi_statusbar_null_outlined; } From dd229b40d3500b1af7be763080751d431e302fbc Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 02:43:39 +0800 Subject: [PATCH 107/458] bump get_it to 7.7.0 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 737ff5936..b801f1201 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -953,10 +953,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: ae30b28cc73053f79fd46b15f430db16cae22a0554e6cd25333c840b310b0270 + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 url: "https://pub.dev" source: hosted - version: "7.6.9" + version: "7.7.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 61d96ebab..edc0ad1e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: json_annotation: ^4.8.1 copy_with_extension: ^5.0.4 freezed_annotation: ^2.4.1 - get_it: ^7.6.9 + get_it: ^7.7.0 flutter_image_compress: ^2.2.0 # Supporting scrolling screenshot on Android, like MIUI From 387452f70a762340a1218ad7088b2f0fe4648b05 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 03:13:35 +0800 Subject: [PATCH 108/458] [network] complete tip --- assets/l10n/en.yaml | 21 +++++---- lib/design/animation/animated.dart | 1 - lib/lifecycle.dart | 3 +- lib/network/i18n.dart | 8 +++- lib/network/page/index.dart | 39 ++++++++++++---- lib/network/widgets/quick_button.dart | 65 +++++++++++++-------------- 6 files changed, 82 insertions(+), 55 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 06894f1a0..f0e7a4fd2 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -492,17 +492,7 @@ network: networkTool: title: Network tool subtitle: Test connection to the school server - openWlanSettingsBtn: Open WLAN settings - noAccessTip: Unable to access network. Please check network settings on your device, then try again. - connectionFailedError: | - Unable to connect to campus network. Troubleshooting: - 1. Attempt to connect to the school's Wi-Fi, "i-SIT","i-SIT-1x" or "eduroam". - 2. Attempt to launch "EasyConnect" VPN to connect to school. - 3. Attempt to use a self-built proxy server to connect to campus network via HTTP proxy. - If they all didn't work, it's probably due to school server downtime for maintenance or even crash, please try again later. - connectionFailedButCampusNetworkConnected: | - The campus network has been connected, but school server is still inaccessible. - It's probably due to school server downtime for maintenance or even crash, please try again later. + openWifiSettingsBtn: Open Wi-Fi settings campusNetworkConnected: Connected to campus network campusNetworkConnectedByVpn: Connected to campus network by VPN studentRegAvailable: Student registration system available @@ -517,6 +507,15 @@ networkTool: pgRegUnavailableTip: | You can't import timetable from student registration system. The only way to import timetables is to load them from a file on your device. Your exam results can't be updated, but can still be viewed if you have already loaded them once. + troubleshooting: Troubleshooting + studentRegTroubleshooting: | + 1. Attempt to connect to the school's Wi-Fi, "i-SIT","i-SIT-1x" or "eduroam". + 2. Attempt to launch "EasyConnect" VPN to connect to school. + 3. Attempt to use a self-built proxy server to connect to campus network via HTTP proxy. + If they all didn't work, it's probably due to school server downtime for maintenance or even crash, please try again later. + studentRegUnavailableButCampusNetworkConnected: | + The campus network has been connected, but school server is still inaccessible. + It's probably due to school server downtime for maintenance or even crash, please try again later. login: login: Login forgotPwd: Forgot password? diff --git a/lib/design/animation/animated.dart b/lib/design/animation/animated.dart index 907293857..dcc589c7d 100644 --- a/lib/design/animation/animated.dart +++ b/lib/design/animation/animated.dart @@ -1,4 +1,3 @@ -import 'package:flutter/animation.dart'; import 'package:flutter/material.dart'; extension AnimatedEx on Widget { diff --git a/lib/lifecycle.dart b/lib/lifecycle.dart index dfdb035a7..9aeb8f3cf 100644 --- a/lib/lifecycle.dart +++ b/lib/lifecycle.dart @@ -3,4 +3,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; final $key = GlobalKey(); final $oaOnline = StateProvider((ref) => false); -final $schoolNetworkAvailable = StateProvider((ref) => false); +final $campusNetworkAvailable = StateProvider((ref) => false); +final $studentRegAvailable = StateProvider((ref) => false); diff --git a/lib/network/i18n.dart b/lib/network/i18n.dart index 4ad46e2e2..94cc63107 100644 --- a/lib/network/i18n.dart +++ b/lib/network/i18n.dart @@ -16,7 +16,7 @@ class _I18n with CommonI18nMixin { String get subtitle => "$ns.subtitle".tr(); - String get openWlanSettingsBtn => "$ns.openWlanSettingsBtn".tr(); + String get openWifiSettingsBtn => "$ns.openWifiSettingsBtn".tr(); String get connectionFailedError => "$ns.connectionFailedError".tr(); @@ -37,6 +37,12 @@ class _I18n with CommonI18nMixin { String get campusNetworkConnected => "$ns.campusNetworkConnected".tr(); String get campusNetworkConnectedByVpn => "$ns.campusNetworkConnectedByVpn".tr(); + + String get troubleshooting => "$ns.troubleshooting".tr(); + String get studentRegTroubleshooting => "$ns.studentRegTroubleshooting".tr(); + + String get studentRegUnavailableButCampusNetworkConnected => + "$ns.studentRegUnavailableButCampusNetworkConnected".tr(); } class _Easyconnect { diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 4fb564c3d..6c936ced8 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/credentials/entity/user_type.dart'; import 'package:sit/credentials/init.dart'; @@ -9,6 +10,7 @@ import 'package:sit/design/widgets/card.dart'; import 'package:sit/init.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/network/service/network.dart'; +import 'package:sit/network/widgets/quick_button.dart'; import '../utils.dart'; import '../i18n.dart'; @@ -86,12 +88,10 @@ class _NetworkToolPageState extends State { body: CustomScrollView( slivers: [ SliverAppBar.medium( - title: i18n.title.text(), - // bottom: const PreferredSize( - // preferredSize: Size.fromHeight(4), - // child: LinearProgressIndicator(), - // ), - actions: [], + title: [ + i18n.title.text(), + const CircularProgressIndicator.adaptive().sizedAll(16), + ].wrap(caa: WrapCrossAlignment.center, spacing: 16), ), SliverList.list( children: [ @@ -105,10 +105,33 @@ class _NetworkToolPageState extends State { StudentRegConnectivityInfo( connected: studentRegAvailable, ).padSymmetric(v: 16, h: 8).inOutlinedCard().animatedSized(), + if(studentRegAvailable == false && campusNetworkStatus != null) + i18n.studentRegUnavailableButCampusNetworkConnected.text( + style: context.textTheme.bodyLarge, + textAlign: TextAlign.center, + ).padSymmetric(v: 16, h: 8).inOutlinedCard(), + if (studentRegAvailable == false) + [ + i18n.troubleshooting.text(style: context.textTheme.titleMedium), + i18n.studentRegTroubleshooting.text( + style: context.textTheme.bodyLarge, + ) + ].column().padSymmetric(v: 16, h: 8).inOutlinedCard(), ], - ) + ), ], ), + bottomNavigationBar: BottomAppBar( + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8), + children: const [ + LaunchEasyConnectButton(), + SizedBox(width: 8), + OpenWifiSettingsButton(), + ], + ), + ), ); } } @@ -149,7 +172,7 @@ class StudentRegConnectivityInfo extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final userType = ref.watch(CredentialsInit.storage.$oaUserType); final widgets = []; - final connected = this.connected == true; + final connected = this.connected != false; final textTheme = context.textTheme; widgets.add((connected ? i18n.studentRegAvailable : i18n.studentRegUnavailable).text( style: textTheme.titleMedium, diff --git a/lib/network/widgets/quick_button.dart b/lib/network/widgets/quick_button.dart index 9e301ca8a..8f547745a 100644 --- a/lib/network/widgets/quick_button.dart +++ b/lib/network/widgets/quick_button.dart @@ -8,45 +8,44 @@ import '../i18n.dart'; const easyConnectDownloadUrl = "https://vpn1.sit.edu.cn/com/installClient.html"; -class QuickButtons extends StatefulWidget { - const QuickButtons({super.key}); +class LaunchEasyConnectButton extends StatelessWidget { + const LaunchEasyConnectButton({super.key}); @override - State createState() => _QuickButtonsState(); + Widget build(BuildContext context) { + return FilledButton( + child: i18n.easyconnect.launchBtn.text(), + onPressed: () async { + final launched = await guardLaunchUrlString(context, 'sangfor://easyconnect'); + if (!launched) { + if (!context.mounted) return; + final confirm = await context.showDialogRequest( + title: i18n.easyconnect.launchFailed, + desc: i18n.easyconnect.launchFailedDesc, + yes: i18n.download, + no: i18n.cancel, + destructive: true, + ); + if (confirm == true) { + if (!context.mounted) return; + await guardLaunchUrlString(context, easyConnectDownloadUrl); + } + } + }, + ); + } } +class OpenWifiSettingsButton extends StatelessWidget { + const OpenWifiSettingsButton({super.key}); -class _QuickButtonsState extends State { @override Widget build(BuildContext context) { - return [ - FilledButton( - child: i18n.easyconnect.launchBtn.text(), - onPressed: () async { - final launched = await guardLaunchUrlString(context, 'sangfor://easyconnect'); - if (!launched) { - if (!context.mounted) return; - final confirm = await context.showDialogRequest( - title: i18n.easyconnect.launchFailed, - desc: i18n.easyconnect.launchFailedDesc, - yes: i18n.download, - no: i18n.cancel, - destructive: true, - ); - if (confirm == true) { - if (!context.mounted) return; - await guardLaunchUrlString(context, easyConnectDownloadUrl); - } - } - }, - ), - OutlinedButton( - onPressed: () { - AppSettings.openAppSettings(type: AppSettingsType.wifi); - }, - child: i18n.openWlanSettingsBtn.text(), - ), - ].row( - maa: MainAxisAlignment.spaceEvenly, + return OutlinedButton( + onPressed: () { + AppSettings.openAppSettings(type: AppSettingsType.wifi); + }, + child: i18n.openWifiSettingsBtn.text(), ); } } + From 04e9375c563a7c45fdc9c9bc47c95f2b265ab3ca Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 03:35:46 +0800 Subject: [PATCH 109/458] [network] l10n of network tool --- assets/l10n/en.yaml | 5 +++-- assets/l10n/zh-Hans.yaml | 28 +++++++++++++++++--------- assets/l10n/zh-Hant.yaml | 40 +++++++++++++++++++++++-------------- lib/network/i18n.dart | 3 +++ lib/network/page/index.dart | 30 ++++++++++++++++++---------- 5 files changed, 69 insertions(+), 37 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index f0e7a4fd2..6c5102c06 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -494,6 +494,7 @@ networkTool: subtitle: Test connection to the school server openWifiSettingsBtn: Open Wi-Fi settings campusNetworkConnected: Connected to campus network + campusNetworkNotConnected: Not connected to campus network campusNetworkConnectedByVpn: Connected to campus network by VPN studentRegAvailable: Student registration system available studentRegUnavailable: Student registration system unavailable @@ -501,12 +502,12 @@ networkTool: You can import timetables from student registration system, and check your exam arrangements and exam results (including GPA). ugRegUnavailableTip: | You can't import timetables from student registration system. The only way to import timetables is to load them from a file on your device. - Your exam arrangements, and exam results (including GPA) can't be updated, but can still be viewed if you have already loaded them once. + Your exam arrangements, and exam results (including GPA) can't be loaded and updated in time, but can still be viewed if you have already loaded them once. pgRegAvailableTip: | You can import timetables from student registration system, and check your exam results. pgRegUnavailableTip: | You can't import timetable from student registration system. The only way to import timetables is to load them from a file on your device. - Your exam results can't be updated, but can still be viewed if you have already loaded them once. + Your exam results can't be loaded and updated in time, but can still be viewed if you have already loaded them once. troubleshooting: Troubleshooting studentRegTroubleshooting: | 1. Attempt to connect to the school's Wi-Fi, "i-SIT","i-SIT-1x" or "eduroam". diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index 9d0635540..0dc33cac4 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -489,21 +489,31 @@ network: networkTool: title: 网络工具 subtitle: 测试与学校服务器的连接 - openWlanSettingsBtn: 打开 WLAN 设置 - noAccessTip: 无法连接到网络,请检查你设备的网络设置后再试。 - connectionFailedError: | - 无法连接到校园网。疑难解答: + openWifiSettingsBtn: 打开 Wi-Fi 设置 + campusNetworkConnected: 已连接到校园网 + campusNetworkNotConnected: 未连接校园网 + campusNetworkConnectedByVpn: 已通过 VPN 连接到校园网 + studentRegAvailable: 教务系统可以访问 + studentRegUnavailable: 教务系统不可访问 + ugRegAvailableTip: | + 你可以从教务系统中导入课程表,并查看考试安排和考试成绩(包括绩点) + ugRegUnavailableTip: | + 你无法从教务系统中导入课程表,唯一的导入方法是从你的设备上的文件中加载。 + 你的考试安排和考试成绩(包括绩点)无法加载和及时更新,但如果你已经加载过一次,则仍可查看。 + pgRegAvailableTip: | + 你可以从教务系统中导入课程表,并查看考试成绩 + pgRegUnavailableTip: | + 你无法从教务系统中导入课程表,唯一的导入方法是从你的设备上的文件中加载。 + 你的考试成绩无法加载和及时更新,但如果你已经加载过一次,则仍可查看。 + troubleshooting: 疑难解答 + studentRegTroubleshooting: | 1. 尝试连接学校的Wi-Fi: "i-SIT","i-SIT-1x" 或 "eduroam"。 2. 尝试使用"EasyConnect" VPN 连接校园网。 3. 尝试通过自行搭建的代理服务器使用 HTTP 代理连接校园网。 如果以上都不起作用, 可能因为是学校服务器停机维护或崩溃,请一段时间后重试。 - connectionFailedButCampusNetworkConnected: | + studentRegUnavailableButCampusNetworkConnected: | 已经连接至校园网,但仍然无法访问学校服务器。 可能因为是学校服务器停机维护或崩溃,请一段时间后重试。 - connectedByProxy: 已通过 HTTP 代理连接到校园网 - connectedByVpn: 已通过 VPN 连接到校园网 - connectedByWlan: 已通过 WLAN 连接到校园网 - connectedByEthernet: 已通过以太网连接到校园网 login: login: 登录 forgotPwd: 忘记密码? diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index 806049877..7046907b9 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -296,14 +296,14 @@ class2nd: reviewing: 審核中 applicationResponse: successfulCheck: 檢查成功 - incompleteApplicantInfo: 您的資訊不完整,請填寫 - alreadyApplied: 您已經申請參加此活動 - applicationReachLimitToday: 您今天已達到申請數量上限 + incompleteApplicantInfo: 你的資訊不完整,請填寫 + alreadyApplied: 你已經申請參加此活動 + applicationReachLimitToday: 你今天已達到申請數量上限 activityReachParticipantLimit: 活動報名人數已達上限 activityExpired: 此活動已過期,不再接受申請 - applicantOccupiedWithinActivity: 您在該時間內已經申請了其他活動 - applicantNotPermitted: 您不能申請此活動 - applicantNotIncluding: 您不屬於可以申請活動的人之一 + applicantOccupiedWithinActivity: 你在該時間內已經申請了其他活動 + applicantNotPermitted: 你不能申請此活動 + applicantNotIncluding: 你不屬於可以申請活動的人之一 ywb: title: 應網辦 info: | @@ -489,21 +489,31 @@ network: networkTool: title: 網路工具 subtitle: 測試與學校伺服器的連線 - openWlanSettingsBtn: 打開 WLAN 設定 - noAccessTip: 無法連線到網路,請檢查你的裝置上的網路設定,然後再試一次。 - connectionFailedError: | - 無法連線到校園網路。疑難排解: + openWifiSettingsBtn: 打開 Wi-Fi 設定 + campusNetworkConnected: 已連線校園網路 + campusNetworkNotConnected: 未連線校園網路 + campusNetworkConnectedByVpn: 已通過 VPN 連線校園網路 + studentRegAvailable: 教務系統可用 + studentRegUnavailable: 教務系統不可用 + ugRegAvailableTip: | + 你可以從教務系統匯入課程表,並查看你的考試安排和考試成績(包括GPA)。 + ugRegUnavailableTip: | + 你無法從教務系統匯入課程表,匯入的唯一方法是從裝置上的檔案中載入它們。 + 你的考試安排和考試成績(包括GPA)無法及時加載和更新,但如果你已經加載過一次,仍然可以查看。 + pgRegAvailableTip: | + 你可以從教務系統匯入課程表,並查看你的考試成績。 + pgRegUnavailableTip: | + 你無法從教務系統匯入課程表,匯入的唯一方法是從裝置上的檔案中載入它們。 + 你的考試成績無法及時加載和更新,但如果你已經加載過一次,仍然可以查看。 + troubleshooting: 疑難排解 + studentRegTroubleshooting: | 1. 嘗試連線學校的Wi-Fi: "i-SIT","i-SIT-1x" 或 "eduroam"。 2. 嘗試啟用"EasyConnect" VPN 連線校園網路。 3. 嘗試通過自建的 HTTP Proxy 伺服器連線校園網路。 如果上述都無法工作, 或許因為學校伺服器停機維護或崩潰, 請稍後重試。 - connectionFailedButCampusNetworkConnected: | + studentRegUnavailableButCampusNetworkConnected: | 已經連線到校園網路,但依然無法訪問學校伺服器。 這或許因為學校伺服器停機維護或崩潰, 請稍後重試。 - connectedByProxy: 已通過 HTTP Proxy 連線校園網路。 - connectedByVpn: 已通過 VPN 連線校園網路。 - connectedByWlan: 已通過 WLAN 連線校園網路。 - connectedByEthernet: 已通過乙太網路連線校園網路。 login: login: 登入 forgotPwd: 忘記密碼? diff --git a/lib/network/i18n.dart b/lib/network/i18n.dart index 94cc63107..78b1a8b1a 100644 --- a/lib/network/i18n.dart +++ b/lib/network/i18n.dart @@ -36,9 +36,12 @@ class _I18n with CommonI18nMixin { String get campusNetworkConnected => "$ns.campusNetworkConnected".tr(); + String get campusNetworkNotConnected => "$ns.campusNetworkNotConnected".tr(); + String get campusNetworkConnectedByVpn => "$ns.campusNetworkConnectedByVpn".tr(); String get troubleshooting => "$ns.troubleshooting".tr(); + String get studentRegTroubleshooting => "$ns.studentRegTroubleshooting".tr(); String get studentRegUnavailableButCampusNetworkConnected => diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 6c936ced8..6369bc06b 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -105,16 +105,19 @@ class _NetworkToolPageState extends State { StudentRegConnectivityInfo( connected: studentRegAvailable, ).padSymmetric(v: 16, h: 8).inOutlinedCard().animatedSized(), - if(studentRegAvailable == false && campusNetworkStatus != null) - i18n.studentRegUnavailableButCampusNetworkConnected.text( - style: context.textTheme.bodyLarge, - textAlign: TextAlign.center, - ).padSymmetric(v: 16, h: 8).inOutlinedCard(), + if (studentRegAvailable == false && campusNetworkStatus != null) + i18n.studentRegUnavailableButCampusNetworkConnected + .text( + style: context.textTheme.bodyLarge, + textAlign: TextAlign.center, + ) + .padSymmetric(v: 16, h: 8) + .inOutlinedCard(), if (studentRegAvailable == false) [ i18n.troubleshooting.text(style: context.textTheme.titleMedium), i18n.studentRegTroubleshooting.text( - style: context.textTheme.bodyLarge, + style: context.textTheme.bodyMedium, ) ].column().padSymmetric(v: 16, h: 8).inOutlinedCard(), ], @@ -180,7 +183,7 @@ class StudentRegConnectivityInfo extends ConsumerWidget { Widget buildTip(String tip) { return tip.text( textAlign: TextAlign.center, - style: textTheme.bodyLarge, + style: textTheme.bodyMedium, ); } @@ -213,20 +216,25 @@ class CampusNetworkConnectivityInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final style = context.textTheme.bodyLarge; + final style = context.textTheme.bodyMedium; final status = this.status; - String? ip = i18n.unknown; + String? ip; String? studentId; if (status != null) { ip = status.ip; studentId = status.studentId ?? i18n.unknown; } return [ - (useVpn ? i18n.campusNetworkConnectedByVpn : i18n.campusNetworkConnected).text( + (status == null + ? i18n.campusNetworkNotConnected + : useVpn + ? i18n.campusNetworkConnectedByVpn + : i18n.campusNetworkConnected) + .text( style: context.textTheme.titleMedium, ), if (studentId != null) "${i18n.credentials.studentId}: $studentId".text(style: style), - "${i18n.network.ipAddress}: $ip".text(style: style), + if (ip != null) "${i18n.network.ipAddress}: $ip".text(style: style), ].column(caa: CrossAxisAlignment.center); } } From 2573d70533819882debc71a0a71d6f1bad06f7c9 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 03:43:40 +0800 Subject: [PATCH 110/458] added animated_size_and_fade --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index b801f1201..9561d596d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + animated_size_and_fade: + dependency: "direct main" + description: + name: animated_size_and_fade + sha256: "374fac69541eab2c8e4c466cdebd8a1f9203da0a14656213d2a074aacfc9adb3" + url: "https://pub.dev" + source: hosted + version: "4.0.0" animations: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index edc0ad1e0..f93cd2791 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,6 +138,7 @@ dependencies: # iCalendar file generator enough_icalendar: ^0.16.0 super_context_menu: ^0.8.11 + animated_size_and_fade: ^4.0.0 # Utils # dart.io.Platform API for Web From 4c2fa27d9ec464fe82eada98c987f69f0a481192 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 04:02:17 +0800 Subject: [PATCH 111/458] msix --- pubspec.lock | 24 ++++++++++++++++++++++++ pubspec.yaml | 13 +++++++++++++ windows/runner/Runner.rc | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 9561d596d..c0ba9817e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.1" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clock: dependency: transitive description: @@ -313,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + console: + dependency: transitive + description: + name: console + sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a + url: "https://pub.dev" + source: hosted + version: "4.1.0" convert: dependency: transitive description: @@ -1341,6 +1357,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + msix: + dependency: "direct dev" + description: + name: msix + sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047" + url: "https://pub.dev" + source: hosted + version: "3.16.7" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f93cd2791..8c58eb3d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -169,6 +169,7 @@ dev_dependencies: freezed: ^2.5.2 copy_with_extension_gen: ^5.0.4 test: ^1.24.9 + msix: ^3.16.7 # ------------------------------------------------------------------------------ @@ -192,3 +193,15 @@ flutter: flutter_intl: enabled: true + +msix_config: + display_name: SIT Life + identity_name: life.mysit.SITLife + msix_version: 1.0.0.0 + logo_path: android/app/src/main/res/mipmap/icon.png + capabilities: internetClient, microphone, webcam + languages: en,zh-hans,zh-hant + file_extension: .timetable + protocol_activation: sitlife + execution_alias: sitlife + output_name: SIT-Life diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 79a810cd0..7932f0169 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -89,7 +89,7 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "life.mysit" "\0" + VALUE "CompanyName", "Liplum Dev" "\0" VALUE "FileDescription", "SIT Life" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "life.mysit.SITLife" "\0" From a51fc9475ddb2112b9cf83f661ec0daae2583565 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 04:22:10 +0800 Subject: [PATCH 112/458] [dev] debug app links --- lib/app.dart | 9 ++++++--- lib/settings/page/developer.dart | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 944fcb5b2..9b36bb9ee 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -19,6 +19,8 @@ import 'package:sit/update/utils.dart'; import 'package:sit/utils/color.dart'; import 'package:system_theme/system_theme.dart'; +final $appLinks = StateProvider((ref) => []); + class MimirApp extends ConsumerStatefulWidget { const MimirApp({super.key}); @@ -117,7 +119,7 @@ class _MimirAppState extends ConsumerState { } } -class _PostServiceRunner extends StatefulWidget { +class _PostServiceRunner extends ConsumerStatefulWidget { final Widget child; const _PostServiceRunner({ @@ -126,10 +128,10 @@ class _PostServiceRunner extends StatefulWidget { }); @override - State<_PostServiceRunner> createState() => _PostServiceRunnerState(); + ConsumerState<_PostServiceRunner> createState() => _PostServiceRunnerState(); } -class _PostServiceRunnerState extends State<_PostServiceRunner> { +class _PostServiceRunnerState extends ConsumerState<_PostServiceRunner> { StreamSubscription? $appLink; @override @@ -146,6 +148,7 @@ class _PostServiceRunnerState extends State<_PostServiceRunner> { } WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { $appLink = AppLinks().allUriLinkStream.listen((uri) async { + ref.read($appLinks.notifier).state = [...ref.read($appLinks), uri]; final navigateCtx = $key.currentContext; if (navigateCtx == null) return; await onHandleQrCodeUriData(context: navigateCtx, qrCodeData: uri); diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index 7c1d01aec..ecca731c8 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:sit/app.dart'; import 'package:sit/credentials/entity/credential.dart'; import 'package:sit/credentials/entity/login_status.dart'; import 'package:sit/credentials/entity/user_type.dart'; @@ -79,6 +80,7 @@ class _DeveloperOptionsPageState extends ConsumerState { context.go("/"); }, ), + const AppLinksTile(), const DebugGoRouteTile(), ]), ), @@ -133,6 +135,24 @@ class _DeveloperOptionsPageState extends ConsumerState { } } +class AppLinksTile extends ConsumerWidget { + const AppLinksTile({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appLinks = ref.watch($appLinks); + return AnimatedExpansionTile( + leading: const Icon(Icons.link), + title: "App links".text(), + children: appLinks + .map((uri) => ListTile( + title: uri.toString().text(), + )) + .toList(), + ); + } +} + class DebugGoRouteTile extends StatefulWidget { const DebugGoRouteTile({super.key}); From 19d44ff02840b69e07dd12f22443e040f50d66ca Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 04:40:13 +0800 Subject: [PATCH 113/458] handle files from app links --- lib/app.dart | 15 +++++++++++++-- lib/files/handle.dart | 5 +++++ lib/settings/page/developer.dart | 4 +++- pubspec.yaml | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 lib/files/handle.dart diff --git a/lib/app.dart b/lib/app.dart index 9b36bb9ee..d42151309 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:ui'; import 'package:animations/animations.dart'; @@ -9,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/files.dart'; +import 'package:sit/files/handle.dart'; import 'package:sit/lifecycle.dart'; import 'package:sit/qrcode/handle.dart'; import 'package:sit/r.dart'; @@ -19,7 +21,7 @@ import 'package:sit/update/utils.dart'; import 'package:sit/utils/color.dart'; import 'package:system_theme/system_theme.dart'; -final $appLinks = StateProvider((ref) => []); +final $appLinks = StateProvider((ref) => <({Uri uri, DateTime ts})>[]); class MimirApp extends ConsumerStatefulWidget { const MimirApp({super.key}); @@ -148,9 +150,18 @@ class _PostServiceRunnerState extends ConsumerState<_PostServiceRunner> { } WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { $appLink = AppLinks().allUriLinkStream.listen((uri) async { - ref.read($appLinks.notifier).state = [...ref.read($appLinks), uri]; + ref.read($appLinks.notifier).state = [...ref.read($appLinks), (uri: uri, ts: DateTime.now())]; final navigateCtx = $key.currentContext; if (navigateCtx == null) return; + if (!kIsWeb) { + final maybePath = uri.toString(); + final isFile = await File(maybePath).exists(); + if (isFile) { + await onHandleFilePath(path: maybePath); + return; + } + } + if (!navigateCtx.mounted) return; await onHandleQrCodeUriData(context: navigateCtx, qrCodeData: uri); }); }); diff --git a/lib/files/handle.dart b/lib/files/handle.dart new file mode 100644 index 000000000..a247cfc87 --- /dev/null +++ b/lib/files/handle.dart @@ -0,0 +1,5 @@ +Future onHandleFilePath({ + required String path, +}) async { + +} diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index ecca731c8..166ed8ec4 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -15,6 +15,7 @@ import 'package:sit/design/adaptive/editor.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/expansion_tile.dart'; import 'package:sit/init.dart'; +import 'package:sit/l10n/extension.dart'; import 'package:sit/login/aggregated.dart'; import 'package:sit/login/utils.dart'; import 'package:sit/r.dart'; @@ -146,7 +147,8 @@ class AppLinksTile extends ConsumerWidget { title: "App links".text(), children: appLinks .map((uri) => ListTile( - title: uri.toString().text(), + title: context.formatYmdhmsNum(uri.ts).text(), + subtitle: uri.uri.toString().text(), )) .toList(), ); diff --git a/pubspec.yaml b/pubspec.yaml index 8c58eb3d3..ebdfead9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -202,6 +202,7 @@ msix_config: capabilities: internetClient, microphone, webcam languages: en,zh-hans,zh-hant file_extension: .timetable - protocol_activation: sitlife + protocol_activation: life.mysit execution_alias: sitlife output_name: SIT-Life + enable_at_startup: true From ccd8de72ef731cc56e0924b0f9b58547cd452073 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 05:06:26 +0800 Subject: [PATCH 114/458] [i18n] move each i18n from settings to individual module --- assets/l10n/en.yaml | 65 +++++++------- assets/l10n/zh-Hans.yaml | 65 +++++++------- assets/l10n/zh-Hant.yaml | 65 +++++++------- lib/game/2048/settings.dart | 3 + lib/game/minesweeper/settings.dart | 3 + lib/l10n/app.dart | 17 ++++ lib/life/i18n.dart | 30 +++++++ .../life.dart => life/page/settings.dart} | 10 +-- lib/route.dart | 6 +- lib/school/i18n.dart | 30 +++++++ .../school.dart => school/page/settings.dart} | 10 +-- lib/settings/i18n.dart | 89 +------------------ lib/settings/page/index.dart | 6 +- lib/timetable/i18n.dart | 24 +++++ .../page/settings.dart} | 18 ++-- 15 files changed, 227 insertions(+), 214 deletions(-) create mode 100644 lib/game/2048/settings.dart create mode 100644 lib/game/minesweeper/settings.dart create mode 100644 lib/l10n/app.dart rename lib/{settings/page/life.dart => life/page/settings.dart} (86%) rename lib/{settings/page/school.dart => school/page/settings.dart} (87%) rename lib/{settings/page/timetable.dart => timetable/page/settings.dart} (82%) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 6c5102c06..5da8ecd97 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -173,6 +173,19 @@ timetable: name: Tomorrow place: Lab teachers: Thomas + settings: + autoUseImported: + title: Auto-use imported + desc: Use the timetable just imported + palette: + title: Palettes + desc: Customize timetable colors + cellStyle: + title: Edit cell style + desc: How course cell looks like + background: + title: Wallpaper + desc: The background of timetable school: navigation: School semester: @@ -204,8 +217,26 @@ school: specializedElective: Specialized elective integratedPractice: Integrated Practice practicalInstruction: Practical instruction + settings: + class2nd: + autoRefresh: + title: Auto-refresh 2nd class scores + desc: Refresh scores when app is launched + examResult: + showResultPreview: + title: Show exam result preview + desc: Show the current semester's exam results in the card life: navigation: Life + settings: + electricity: + autoRefresh: + title: Auto-refresh electricity + desc: Refresh electricity when app is launched + expenseRecords: + autoRefresh: + title: Auto-refresh expense records + desc: Refresh expense records when app is launched me: navigation: Me class2nd: @@ -613,40 +644,6 @@ settings: reload: title: Reload desc: Reload all modules - timetable: - title: Timetable - autoUseImported: - title: Auto-use imported - desc: Use the timetable just imported - palette: - title: Palettes - desc: Customize timetable colors - cellStyle: - title: Edit cell style - desc: How course cell looks like - background: - title: Wallpaper - desc: The background of timetable - school: - title: School - class2nd: - autoRefresh: - title: Auto-refresh 2nd class scores - desc: Refresh scores when app is launched - examResult: - showResultPreview: - title: Show exam result preview - desc: Show the current semester's exam results in the card - life: - title: Life - electricity: - autoRefresh: - title: Auto-refresh electricity - desc: Refresh electricity when app is launched - expenseRecords: - autoRefresh: - title: Auto-refresh expense records - desc: Refresh expense records when app is launched clearCache: title: Clear cache desc: Clear the offline cache during use diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index 0dc33cac4..6a32da086 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -173,6 +173,19 @@ timetable: name: 明天的 place: 实验室 teachers: 刘老师 + settings: + autoUseImported: + title: 自动使用新课程表 + desc: 自动使用新导入的课程表 + palette: + title: 配色方案 + desc: 自定义课程表的颜色 + cellStyle: + title: 编辑单元格风格 + desc: 课程单元格如何显示 + background: + title: 壁纸 + desc: 课程表的背景 school: navigation: 学校 semester: @@ -201,8 +214,26 @@ school: specializedElective: 专业选修课 integratedPractice: 综合实践 practicalInstruction: 实践教学 + settings: + class2nd: + autoRefresh: + title: 第二课堂分自动刷新 + desc: 启动时刷新第二课堂分 + examResult: + showResultPreview: + title: 显示考试成绩预览 + desc: 在卡片中显示当前学期的考试成绩 life: navigation: 生活 + settings: + electricity: + autoRefresh: + title: 电费余额自动刷新 + desc: 启动时刷新电费余额 + expenseRecords: + autoRefresh: + title: 消费记录自动刷新 + desc: 启动时刷新消费记录 me: navigation: 个人 class2nd: @@ -613,40 +644,6 @@ settings: reload: title: 重新加载 desc: 重新加载所有模块 - timetable: - title: 课程表 - autoUseImported: - title: 自动使用新课程表 - desc: 自动使用新导入的课程表 - palette: - title: 配色方案 - desc: 自定义课程表的颜色 - cellStyle: - title: 编辑单元格风格 - desc: 课程单元格如何显示 - background: - title: 壁纸 - desc: 课程表的背景 - school: - title: 学校 - class2nd: - autoRefresh: - title: 第二课堂分自动刷新 - desc: 启动时刷新第二课堂分 - examResult: - showResultPreview: - title: 显示考试成绩预览 - desc: 在卡片中显示当前学期的考试成绩 - life: - title: 生活 - electricity: - autoRefresh: - title: 电费余额自动刷新 - desc: 启动时刷新电费余额 - expenseRecords: - autoRefresh: - title: 消费记录自动刷新 - desc: 启动时刷新消费记录 clearCache: title: 清空缓存 desc: 清空使用过程中产生的离线缓存 diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index 7046907b9..a8049527f 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -174,6 +174,19 @@ timetable: name: 明天的 place: 實驗室 teachers: 劉老師 + settings: + autoUseImported: + title: 自動使用匯入的 + desc: 使用新匯入的課程表 + palette: + title: 調色盤 + desc: 自訂課程表的調色盤 + cellStyle: + title: 編輯單元樣式 + desc: 課程單元看上去如何 + background: + title: 桌布 + desc: 課程表的背景 school: navigation: 學校 semester: @@ -201,8 +214,26 @@ school: specializedElective: 專業選修課 integratedPractice: 綜合實踐 practicalInstruction: 實踐教學 + settings: + class2nd: + autoRefresh: + title: 第二課堂分自動重新整理 + desc: 啟動時重新整理第二課堂分 + examResult: + showResultPreview: + title: 顯示考試成績預覽 + desc: 在卡片中顯示當前學期的考試成績 life: navigation: 生活 + settings: + electricity: + autoRefresh: + title: 電費餘額自動重新整理 + desc: 啟動時重新整理電費餘額 + expenseRecords: + autoRefresh: + title: 消費記錄自動重新整理 + desc: 啟動時重新整理消費記錄 me: navigation: 個人 class2nd: @@ -613,40 +644,6 @@ settings: reload: title: 重新載入 desc: 重新載入所有模塊 - timetable: - title: 課程表 - autoUseImported: - title: 自動使用匯入的 - desc: 使用新匯入的課程表 - palette: - title: 調色盤 - desc: 自訂課程表的調色盤 - cellStyle: - title: 編輯單元樣式 - desc: 課程單元看上去如何 - background: - title: 桌布 - desc: 課程表的背景 - school: - title: 學校 - class2nd: - autoRefresh: - title: 第二課堂分自動重新整理 - desc: 啟動時重新整理第二課堂分 - examResult: - showResultPreview: - title: 顯示考試成績預覽 - desc: 在卡片中顯示當前學期的考試成績 - life: - title: 生活 - electricity: - autoRefresh: - title: 電費餘額自動重新整理 - desc: 啟動時重新整理電費餘額 - expenseRecords: - autoRefresh: - title: 消費記錄自動重新整理 - desc: 啟動時重新整理消費記錄 clearCache: title: 清空快取 desc: 清空使用過程中產生的離線緩存 diff --git a/lib/game/2048/settings.dart b/lib/game/2048/settings.dart new file mode 100644 index 000000000..74490a1c3 --- /dev/null +++ b/lib/game/2048/settings.dart @@ -0,0 +1,3 @@ +class Settings2048 { + const Settings2048(); +} diff --git a/lib/game/minesweeper/settings.dart b/lib/game/minesweeper/settings.dart new file mode 100644 index 000000000..e438100ba --- /dev/null +++ b/lib/game/minesweeper/settings.dart @@ -0,0 +1,3 @@ +class SettingsMinesweeper { + const SettingsMinesweeper(); +} diff --git a/lib/l10n/app.dart b/lib/l10n/app.dart new file mode 100644 index 000000000..e0b204742 --- /dev/null +++ b/lib/l10n/app.dart @@ -0,0 +1,17 @@ +import 'package:sit/timetable/i18n.dart' as t; +import 'package:sit/school/i18n.dart' as s; +import 'package:sit/life/i18n.dart' as l; + +class AppI18n { + const AppI18n(); + final navigation = const _Navigation(); +} + +class _Navigation { + const _Navigation(); + + String get timetable => t.i18n.navigation; + String get school => s.i18n.navigation; + String get life => l.i18n.navigation; + // String get game => t.i18n.navigation; +} diff --git a/lib/life/i18n.dart b/lib/life/i18n.dart index 91287ff56..dd3fbd645 100644 --- a/lib/life/i18n.dart +++ b/lib/life/i18n.dart @@ -6,7 +6,37 @@ const i18n = _I18n(); class _I18n with CommonI18nMixin { const _I18n(); + final settings = const _Settings(); + static const ns = "life"; String get navigation => "$ns.navigation".tr(); } + +class _Settings { + const _Settings(); + + final electricity = const _Electricity(); + final expense = const _Expense(); + static const ns = "${_I18n.ns}.settings"; +} + +class _Electricity { + static const ns = "${_Settings.ns}.electricity"; + + const _Electricity(); + + String get autoRefresh => "$ns.autoRefresh.title".tr(); + + String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); +} + +class _Expense { + static const ns = "${_Settings.ns}.expenseRecords"; + + const _Expense(); + + String get autoRefresh => "$ns.autoRefresh.title".tr(); + + String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); +} diff --git a/lib/settings/page/life.dart b/lib/life/page/settings.dart similarity index 86% rename from lib/settings/page/life.dart rename to lib/life/page/settings.dart index c5bfcd999..3fb5661f2 100644 --- a/lib/settings/page/life.dart +++ b/lib/life/page/settings.dart @@ -24,7 +24,7 @@ class _LifeSettingsPageState extends State { pinned: true, snap: false, floating: false, - title: i18n.life.title.text(), + title: i18n.navigation.text(), ), SliverList( delegate: SliverChildListDelegate([ @@ -40,8 +40,8 @@ class _LifeSettingsPageState extends State { Widget buildElectricityAutoRefreshToggle() { return StatefulBuilder( builder: (ctx, setState) => ListTile( - title: i18n.life.electricity.autoRefresh.text(), - subtitle: i18n.life.electricity.autoRefreshDesc.text(), + title: i18n.settings.electricity.autoRefresh.text(), + subtitle: i18n.settings.electricity.autoRefreshDesc.text(), leading: Icon(context.icons.refresh), trailing: Switch.adaptive( value: Settings.life.electricity.autoRefresh, @@ -58,8 +58,8 @@ class _LifeSettingsPageState extends State { Widget buildExpenseAutoRefreshToggle() { return StatefulBuilder( builder: (ctx, setState) => ListTile( - title: i18n.life.expense.autoRefresh.text(), - subtitle: i18n.life.expense.autoRefreshDesc.text(), + title: i18n.settings.expense.autoRefresh.text(), + subtitle: i18n.settings.expense.autoRefreshDesc.text(), leading: Icon(context.icons.refresh), trailing: Switch.adaptive( value: Settings.life.expense.autoRefresh, diff --git a/lib/route.dart b/lib/route.dart index 6dd957867..94a5fda8a 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -11,6 +11,7 @@ import 'package:sit/game/index.dart'; import 'package:sit/game/minesweeper/index.dart'; import 'package:sit/game/suika/index.dart'; import 'package:sit/index.dart'; +import 'package:sit/life/page/settings.dart'; import 'package:sit/lifecycle.dart'; import 'package:sit/me/edu_email/page/login.dart'; import 'package:sit/me/edu_email/page/outbox.dart'; @@ -26,9 +27,8 @@ import 'package:sit/school/ywb/page/service.dart'; import 'package:sit/school/ywb/page/application.dart'; import 'package:sit/settings/page/about.dart'; import 'package:sit/settings/page/language.dart'; -import 'package:sit/settings/page/life.dart'; import 'package:sit/settings/page/proxy.dart'; -import 'package:sit/settings/page/school.dart'; +import 'package:sit/school/page/settings.dart'; import 'package:sit/settings/page/storage.dart'; import 'package:sit/life/expense_records/page/records.dart'; import 'package:sit/life/expense_records/page/statistics.dart'; @@ -43,6 +43,7 @@ import 'package:sit/timetable/page/p13n/background.dart'; import 'package:sit/timetable/page/p13n/cell_style.dart'; import 'package:sit/timetable/page/editor.dart'; import 'package:sit/timetable/page/p13n/palette_editor.dart'; +import 'package:sit/timetable/page/settings.dart'; import 'package:sit/utils/riverpod.dart'; import 'package:sit/widgets/not_found.dart'; import 'package:sit/school/oa_announce/entity/announce.dart'; @@ -61,7 +62,6 @@ import 'package:sit/settings/page/developer.dart'; import 'package:sit/settings/page/index.dart'; import 'package:sit/me/index.dart'; import 'package:sit/school/index.dart'; -import 'package:sit/settings/page/timetable.dart'; import 'package:sit/timetable/page/import.dart'; import 'package:sit/timetable/page/index.dart'; import 'package:sit/timetable/page/mine.dart'; diff --git a/lib/school/i18n.dart b/lib/school/i18n.dart index 75ee0f466..94ce49d80 100644 --- a/lib/school/i18n.dart +++ b/lib/school/i18n.dart @@ -8,6 +8,7 @@ class _I18n with CommonI18nMixin { static const ns = "school"; final course = const CourseI18n(); + final settings = const _Settings(); String get title => "$ns.title".tr(); @@ -41,3 +42,32 @@ class CourseI18n { String get elective => "$ns.elective".tr(); } + + +class _Settings { + const _Settings(); + + static const ns = "${_I18n.ns}.settings"; + final class2nd = const _Class2nd(); + final examResult = const _ExamResult(); +} + +class _Class2nd { + static const ns = "${_Settings.ns}.class2nd"; + + const _Class2nd(); + + String get autoRefresh => "$ns.autoRefresh.title".tr(); + + String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); +} + +class _ExamResult { + static const ns = "${_Settings.ns}.examResult"; + + const _ExamResult(); + + String get showResultPreview => "$ns.showResultPreview.title".tr(); + + String get showResultPreviewDesc => "$ns.showResultPreview.desc".tr(); +} diff --git a/lib/settings/page/school.dart b/lib/school/page/settings.dart similarity index 87% rename from lib/settings/page/school.dart rename to lib/school/page/settings.dart index 0456a7ab8..ad7f24526 100644 --- a/lib/settings/page/school.dart +++ b/lib/school/page/settings.dart @@ -27,7 +27,7 @@ class _SchoolSettingsPageState extends ConsumerState { pinned: true, snap: false, floating: false, - title: i18n.school.title.text(), + title: i18n.navigation.text(), ), SliverList.list( children: [ @@ -43,8 +43,8 @@ class _SchoolSettingsPageState extends ConsumerState { Widget buildClass2ndAutoRefreshToggle() { return StatefulBuilder( builder: (ctx, setState) => ListTile( - title: i18n.school.class2nd.autoRefresh.text(), - subtitle: i18n.school.class2nd.autoRefreshDesc.text(), + title: i18n.settings.class2nd.autoRefresh.text(), + subtitle: i18n.settings.class2nd.autoRefreshDesc.text(), leading: Icon(context.icons.refresh), trailing: Switch.adaptive( value: Settings.school.class2nd.autoRefresh, @@ -61,8 +61,8 @@ class _SchoolSettingsPageState extends ConsumerState { Widget buildExamResultShowResultPreviewToggle() { return StatefulBuilder( builder: (ctx, setState) => ListTile( - title: i18n.school.examResult.showResultPreview.text(), - subtitle: i18n.school.examResult.showResultPreviewDesc.text(), + title: i18n.settings.examResult.showResultPreview.text(), + subtitle: i18n.settings.examResult.showResultPreviewDesc.text(), leading: const Icon(Icons.preview), trailing: Switch.adaptive( value: Settings.school.examResult.showResultPreview, diff --git a/lib/settings/i18n.dart b/lib/settings/i18n.dart index 3fbd895bd..0bb590573 100644 --- a/lib/settings/i18n.dart +++ b/lib/settings/i18n.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:sit/credentials/i18n.dart'; +import 'package:sit/l10n/app.dart'; import 'package:sit/l10n/common.dart'; const i18n = _I18n(); @@ -10,10 +11,8 @@ class _I18n with CommonI18nMixin { final oaCredentials = const _OaCredentials(); final proxy = const _Proxy(); final dev = const _DevOptions(); - final timetable = const _Timetable(); - final school = const _School(); - final life = const _Life(); final about = const _About(); + final app = const AppI18n(); static const ns = "settings"; @@ -80,70 +79,6 @@ class _Proxy { String get setFromQrCodeDesc => "$ns.setFromQrCodeDesc".tr(); } -class _Timetable { - const _Timetable(); - - static const ns = "${_I18n.ns}.timetable"; - - String get title => "$ns.title".tr(); - - String get autoUseImported => "$ns.autoUseImported.title".tr(); - - String get autoUseImportedDesc => "$ns.autoUseImported.desc".tr(); - - String get palette => "$ns.palette.title".tr(); - - String get paletteDesc => "$ns.palette.desc".tr(); - - String get cellStyle => "$ns.cellStyle.title".tr(); - - String get cellStyleDesc => "$ns.cellStyle.desc".tr(); - - String get background => "$ns.background.title".tr(); - - String get backgroundDesc => "$ns.background.desc".tr(); -} - -class _School { - const _School(); - - static const ns = "${_I18n.ns}.school"; - final class2nd = const _Class2nd(); - final examResult = const _ExamResult(); - - String get title => "$ns.title".tr(); -} - -class _Class2nd { - static const ns = "${_School.ns}.class2nd"; - - const _Class2nd(); - - String get autoRefresh => "$ns.autoRefresh.title".tr(); - - String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); -} - -class _ExamResult { - static const ns = "${_School.ns}.examResult"; - - const _ExamResult(); - - String get showResultPreview => "$ns.showResultPreview.title".tr(); - - String get showResultPreviewDesc => "$ns.showResultPreview.desc".tr(); -} - -class _Life { - const _Life(); - - final electricity = const _Electricity(); - final expense = const _Expense(); - static const ns = "${_I18n.ns}.life"; - - String get title => "$ns.title".tr(); -} - class _About { const _About(); @@ -162,26 +97,6 @@ class _About { String get checkUpdate => "$ns.checkUpdate".tr(); } -class _Electricity { - static const ns = "${_Life.ns}.electricity"; - - const _Electricity(); - - String get autoRefresh => "$ns.autoRefresh.title".tr(); - - String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); -} - -class _Expense { - static const ns = "${_Life.ns}.expenseRecords"; - - const _Expense(); - - String get autoRefresh => "$ns.autoRefresh.title".tr(); - - String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); -} - class _DevOptions { const _DevOptions(); diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index b0beed7c9..88a2e2c1e 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -97,17 +97,17 @@ class _SettingsPageState extends ConsumerState { if (loginStatus != LoginStatus.never) { all.add(PageNavigationTile( leading: const Icon(Icons.calendar_month_outlined), - title: i18n.timetable.title.text(), + title: i18n.app.navigation.timetable.text(), path: "/settings/timetable", )); if (!kIsWeb) { all.add(PageNavigationTile( - title: i18n.school.title.text(), + title: i18n.app.navigation.school.text(), leading: const Icon(Icons.school_outlined), path: "/settings/school", )); all.add(PageNavigationTile( - title: i18n.life.title.text(), + title: i18n.app.navigation.life.text(), leading: const Icon(Icons.spa_outlined), path: "/settings/life", )); diff --git a/lib/timetable/i18n.dart b/lib/timetable/i18n.dart index f3ddd51da..705447162 100644 --- a/lib/timetable/i18n.dart +++ b/lib/timetable/i18n.dart @@ -18,6 +18,7 @@ class _I18n with CommonI18nMixin { final editor = const _Editor(); final freeTip = const _FreeTip(); final campus = const CampusI10n(); + final settings = const _Settings(); String get navigation => "$ns.navigation".tr(); @@ -316,3 +317,26 @@ class _FreeTip { String get findNearestDayWithClass => "$ns.findNearestDayWithClass".tr(); } + + +class _Settings { + const _Settings(); + + static const ns = "${_I18n.ns}.settings"; + + String get autoUseImported => "$ns.autoUseImported.title".tr(); + + String get autoUseImportedDesc => "$ns.autoUseImported.desc".tr(); + + String get palette => "$ns.palette.title".tr(); + + String get paletteDesc => "$ns.palette.desc".tr(); + + String get cellStyle => "$ns.cellStyle.title".tr(); + + String get cellStyleDesc => "$ns.cellStyle.desc".tr(); + + String get background => "$ns.background.title".tr(); + + String get backgroundDesc => "$ns.background.desc".tr(); +} diff --git a/lib/settings/page/timetable.dart b/lib/timetable/page/settings.dart similarity index 82% rename from lib/settings/page/timetable.dart rename to lib/timetable/page/settings.dart index d1ac5f933..7479d0025 100644 --- a/lib/settings/page/timetable.dart +++ b/lib/timetable/page/settings.dart @@ -24,7 +24,7 @@ class _TimetableSettingsPageState extends State { pinned: true, snap: false, floating: false, - title: i18n.timetable.title.text(), + title: i18n.navigation.text(), ), SliverList.list( children: [ @@ -42,8 +42,8 @@ class _TimetableSettingsPageState extends State { Widget buildAutoUseImportedToggle() { return ListTile( - title: i18n.timetable.autoUseImported.text(), - subtitle: i18n.timetable.autoUseImportedDesc.text(), + title: i18n.settings.autoUseImported.text(), + subtitle: i18n.settings.autoUseImportedDesc.text(), leading: const Icon(Icons.auto_mode_outlined), trailing: Switch.adaptive( value: Settings.timetable.autoUseImported, @@ -59,8 +59,8 @@ class _TimetableSettingsPageState extends State { Widget buildP13n() { return ListTile( leading: const Icon(Icons.color_lens_outlined), - title: i18n.timetable.palette.text(), - subtitle: i18n.timetable.paletteDesc.text(), + title: i18n.settings.palette.text(), + subtitle: i18n.settings.paletteDesc.text(), trailing: const Icon(Icons.open_in_new), onTap: () async { await context.push("/timetable/p13n"); @@ -71,8 +71,8 @@ class _TimetableSettingsPageState extends State { Widget buildCellStyle() { return ListTile( leading: const Icon(Icons.view_comfortable_outlined), - title: i18n.timetable.cellStyle.text(), - subtitle: i18n.timetable.cellStyleDesc.text(), + title: i18n.settings.cellStyle.text(), + subtitle: i18n.settings.cellStyleDesc.text(), trailing: const Icon(Icons.open_in_new), onTap: () async { await context.push("/timetable/cell-style"); @@ -83,8 +83,8 @@ class _TimetableSettingsPageState extends State { Widget buildBackground() { return ListTile( leading: const Icon(Icons.image_outlined), - title: i18n.timetable.background.text(), - subtitle: i18n.timetable.backgroundDesc.text(), + title: i18n.settings.background.text(), + subtitle: i18n.settings.backgroundDesc.text(), trailing: const Icon(Icons.open_in_new), onTap: () async { await context.push("/timetable/background"); From eaea9875871c1620df1e7f34e9c3ba81eec56140 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 05:17:17 +0800 Subject: [PATCH 115/458] [workflow] build windows msix --- .github/workflows/windows.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index bda23442a..978bac77c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -25,10 +25,10 @@ jobs: - name: Build Windows run: | flutter pub run build_runner build --delete-conflicting-outputs - flutter build windows + dart run msix:create - name: Upload building uses: actions/upload-artifact@v4 with: - name: Windows-x86_64.zip - path: build\windows\x64\runner\Release + name: SITLife-Windows-release + path: build\windows\x64\runner\Release\SIT-Life.msix From ea27de726faa8959e5fb4ce0c12b64f61bad75cc Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 05:22:30 +0800 Subject: [PATCH 116/458] [game] i18n of enableHapticFeedback --- lib/game/2048/i18n.dart | 6 ++++++ lib/game/i18n.dart | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/game/2048/i18n.dart b/lib/game/2048/i18n.dart index e552e4f1d..0fb3a7e97 100644 --- a/lib/game/2048/i18n.dart +++ b/lib/game/2048/i18n.dart @@ -15,3 +15,9 @@ class _I18n with CommonI18nMixin, CommonGameI18nMixin { String get best => "$ns.best".tr(); } + +class _Settings { + const _Settings(); + + static const ns = "${_I18n.ns}.settings"; +} diff --git a/lib/game/i18n.dart b/lib/game/i18n.dart index 96d52e1c6..56095a95d 100644 --- a/lib/game/i18n.dart +++ b/lib/game/i18n.dart @@ -23,3 +23,11 @@ mixin class CommonGameI18nMixin { String get gameOver => "$_ns.gameOver".tr(); } + +mixin class CommonGameSettingsI18nMixin { + static const ns = "$_ns.settings"; + + String get enableHapticFeedback => "$ns.enableHapticFeedback".tr(); + + String get enableHapticFeedbackDesc => "$ns.enableHapticFeedbackDesc".tr(); +} From dc259156dff60a530a6572527665b290130b91be Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 05:23:42 +0800 Subject: [PATCH 117/458] updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6aab826..807e935f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## v2.4.0 - Redesigned the expense statistics page. +- Redesigned the network tool page. ## v2.3.0 - The 2048 game supports save/load feature. From 53911e27de8cdc87442b3b8983760c4d5fcccba0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Apr 2024 21:45:31 +0000 Subject: [PATCH 118/458] build: 2.4.0+29 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ebdfead9f..fdee733f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A multiplatform app for SIT students." # The build version numbers is incremented automatically. # DO NOT DIRECTLY CHANGE IT -version: 2.4.0+28 +version: 2.4.0+29 homepage: https://github.com/liplum/mimir repository: https://github.com/liplum/mimir From 8ea81624f2e0f37090963162942507276b26c96a Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 10:08:38 +0800 Subject: [PATCH 119/458] removed parameter BuildContext in handleRequestError --- lib/files/handle.dart | 4 +--- lib/life/expense_records/page/records.dart | 2 +- lib/me/edu_email/page/inbox.dart | 4 ++-- lib/network/widgets/quick_button.dart | 2 +- lib/school/class2nd/index.dart | 2 +- lib/school/class2nd/page/activity.dart | 2 +- lib/school/class2nd/page/attended.dart | 2 +- lib/school/class2nd/page/details.dart | 4 ++-- lib/school/exam_arrange/page/list.dart | 2 +- lib/school/exam_result/page/gpa.dart | 2 +- lib/school/exam_result/page/result.pg.dart | 2 +- lib/school/exam_result/page/result.ug.dart | 2 +- lib/school/i18n.dart | 1 - lib/school/library/page/borrowing.dart | 2 +- lib/school/library/page/details.dart | 4 ++-- lib/school/library/page/history.dart | 2 +- lib/school/library/page/login.dart | 2 +- lib/school/library/page/search_result.dart | 2 +- lib/school/library/utils.dart | 2 +- lib/school/library/widgets/book.dart | 2 +- lib/school/oa_announce/page/details.dart | 2 +- lib/school/oa_announce/page/list.dart | 2 +- lib/school/oa_announce/widget/attachment.dart | 2 +- lib/school/ywb/page/application.dart | 2 +- lib/school/ywb/page/details.dart | 2 +- lib/school/ywb/page/service.dart | 2 +- lib/session/sso.dart | 1 - lib/timetable/i18n.dart | 1 - lib/update/utils.dart | 2 +- lib/utils/error.dart | 6 ++++-- 30 files changed, 33 insertions(+), 36 deletions(-) diff --git a/lib/files/handle.dart b/lib/files/handle.dart index a247cfc87..bf5a4e637 100644 --- a/lib/files/handle.dart +++ b/lib/files/handle.dart @@ -1,5 +1,3 @@ Future onHandleFilePath({ required String path, -}) async { - -} +}) async {} diff --git a/lib/life/expense_records/page/records.dart b/lib/life/expense_records/page/records.dart index 10b8751d8..c31a0060d 100644 --- a/lib/life/expense_records/page/records.dart +++ b/lib/life/expense_records/page/records.dart @@ -59,7 +59,7 @@ class _ExpenseRecordsPageState extends ConsumerState { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); setState(() { isFetching = false; }); diff --git a/lib/me/edu_email/page/inbox.dart b/lib/me/edu_email/page/inbox.dart index a995c350c..08d4850f7 100644 --- a/lib/me/edu_email/page/inbox.dart +++ b/lib/me/edu_email/page/inbox.dart @@ -35,7 +35,7 @@ class _EduEmailInboxPageState extends ConsumerState { try { await EduEmailInit.service.login(credentials); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); CredentialsInit.storage.eduEmailCredentials = null; return; } @@ -51,7 +51,7 @@ class _EduEmailInboxPageState extends ConsumerState { this.messages = messages; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); } } diff --git a/lib/network/widgets/quick_button.dart b/lib/network/widgets/quick_button.dart index 8f547745a..0d071490b 100644 --- a/lib/network/widgets/quick_button.dart +++ b/lib/network/widgets/quick_button.dart @@ -35,6 +35,7 @@ class LaunchEasyConnectButton extends StatelessWidget { ); } } + class OpenWifiSettingsButton extends StatelessWidget { const OpenWifiSettingsButton({super.key}); @@ -48,4 +49,3 @@ class OpenWifiSettingsButton extends StatelessWidget { ); } } - diff --git a/lib/school/class2nd/index.dart b/lib/school/class2nd/index.dart index e27858d06..b9b9251bf 100644 --- a/lib/school/class2nd/index.dart +++ b/lib/school/class2nd/index.dart @@ -63,7 +63,7 @@ class _Class2ndAppCardState extends ConsumerState { context.showSnackBar(content: i18n.refreshSuccessTip.text()); } } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; if (active) { context.showSnackBar(content: i18n.refreshFailedTip.text()); diff --git a/lib/school/class2nd/page/activity.dart b/lib/school/class2nd/page/activity.dart index e93bb9dfb..ee692f1eb 100644 --- a/lib/school/class2nd/page/activity.dart +++ b/lib/school/class2nd/page/activity.dart @@ -196,7 +196,7 @@ class _ActivityLoadingListState extends State with Automati }); widget.onLoadingChanged(false); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/class2nd/page/attended.dart b/lib/school/class2nd/page/attended.dart index 548f6a495..be7896339 100644 --- a/lib/school/class2nd/page/attended.dart +++ b/lib/school/class2nd/page/attended.dart @@ -76,7 +76,7 @@ class _AttendedActivityPageState extends ConsumerState { }); $loadingProgress.value = 0; } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() => isFetching = false); $loadingProgress.value = 0; diff --git a/lib/school/class2nd/page/details.dart b/lib/school/class2nd/page/details.dart index 3b2758eb0..6809978f4 100644 --- a/lib/school/class2nd/page/details.dart +++ b/lib/school/class2nd/page/details.dart @@ -168,7 +168,7 @@ class _Class2ndActivityDetailsPageState extends State { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/exam_result/page/result.pg.dart b/lib/school/exam_result/page/result.pg.dart index 76ab0963c..b9e9be3b0 100644 --- a/lib/school/exam_result/page/result.pg.dart +++ b/lib/school/exam_result/page/result.pg.dart @@ -58,7 +58,7 @@ class _ExamResultPgPageState extends State { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/exam_result/page/result.ug.dart b/lib/school/exam_result/page/result.ug.dart index 09c166856..49fc29b84 100644 --- a/lib/school/exam_result/page/result.ug.dart +++ b/lib/school/exam_result/page/result.ug.dart @@ -66,7 +66,7 @@ class _ExamResultUgPageState extends ConsumerState { }); $loadingProgress.value = 0; } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/i18n.dart b/lib/school/i18n.dart index 94ce49d80..76e113355 100644 --- a/lib/school/i18n.dart +++ b/lib/school/i18n.dart @@ -43,7 +43,6 @@ class CourseI18n { String get elective => "$ns.elective".tr(); } - class _Settings { const _Settings(); diff --git a/lib/school/library/page/borrowing.dart b/lib/school/library/page/borrowing.dart index f2dce2d20..0a9b7e2b7 100644 --- a/lib/school/library/page/borrowing.dart +++ b/lib/school/library/page/borrowing.dart @@ -47,7 +47,7 @@ class _LibraryBorrowingPageState extends State { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/library/page/details.dart b/lib/school/library/page/details.dart index baea21c3a..d7b035147 100644 --- a/lib/school/library/page/details.dart +++ b/lib/school/library/page/details.dart @@ -59,7 +59,7 @@ class _BookDetailsPageState extends State { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); return; } if (!mounted) return; @@ -234,7 +234,7 @@ class _BookCollectionPreviewListState extends State { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); return; } if (!mounted) return; diff --git a/lib/school/library/page/history.dart b/lib/school/library/page/history.dart index 442e147df..0c59f51f4 100644 --- a/lib/school/library/page/history.dart +++ b/lib/school/library/page/history.dart @@ -43,7 +43,7 @@ class _LibraryMyBorrowingHistoryPageState extends State { setState(() => isLoggingIn = false); context.replace("/library/borrowing"); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() => isLoggingIn = false); if (error is Exception) { diff --git a/lib/school/library/page/search_result.dart b/lib/school/library/page/search_result.dart index 8d3c27f50..24d989900 100644 --- a/lib/school/library/page/search_result.dart +++ b/lib/school/library/page/search_result.dart @@ -85,7 +85,7 @@ class _BookSearchResultWidgetState extends State with Au isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/library/utils.dart b/lib/school/library/utils.dart index dbc22daf0..f1a1b4a14 100644 --- a/lib/school/library/utils.dart +++ b/lib/school/library/utils.dart @@ -11,6 +11,6 @@ Future renewBorrowedBook(BuildContext context, String barcode) async { if (!context.mounted) return; await context.showTip(title: i18n.borrowing.renew, ok: i18n.ok, desc: result); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); } } diff --git a/lib/school/library/widgets/book.dart b/lib/school/library/widgets/book.dart index 3860b1652..03d654a1d 100644 --- a/lib/school/library/widgets/book.dart +++ b/lib/school/library/widgets/book.dart @@ -39,7 +39,7 @@ class _AsyncBookImageState extends State { this.image = image; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); } } diff --git a/lib/school/oa_announce/page/details.dart b/lib/school/oa_announce/page/details.dart index 502acb1d7..c338ef636 100644 --- a/lib/school/oa_announce/page/details.dart +++ b/lib/school/oa_announce/page/details.dart @@ -60,7 +60,7 @@ class _AnnounceDetailsPageState extends State { isFetching = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/oa_announce/page/list.dart b/lib/school/oa_announce/page/list.dart index 9ece48cab..e3e0c678e 100644 --- a/lib/school/oa_announce/page/list.dart +++ b/lib/school/oa_announce/page/list.dart @@ -202,7 +202,7 @@ class _OaAnnounceLoadingListState extends State with Auto }); widget.onLoadingChanged(false); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/oa_announce/widget/attachment.dart b/lib/school/oa_announce/widget/attachment.dart index 260abcd24..b95e46e5b 100644 --- a/lib/school/oa_announce/widget/attachment.dart +++ b/lib/school/oa_announce/widget/attachment.dart @@ -91,7 +91,7 @@ class _AttachmentLinkTileState extends State { progress = 1; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { progress = null; diff --git a/lib/school/ywb/page/application.dart b/lib/school/ywb/page/application.dart index e9260b4b4..e81987cd8 100644 --- a/lib/school/ywb/page/application.dart +++ b/lib/school/ywb/page/application.dart @@ -155,7 +155,7 @@ class _YwbApplicationLoadingListState extends State w }); widget.onLoadingChanged(false); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/ywb/page/details.dart b/lib/school/ywb/page/details.dart index e7946108d..f01f5652c 100644 --- a/lib/school/ywb/page/details.dart +++ b/lib/school/ywb/page/details.dart @@ -53,7 +53,7 @@ class _YwbServiceDetailsPageState extends State { details = meta; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isFetching = false; diff --git a/lib/school/ywb/page/service.dart b/lib/school/ywb/page/service.dart index d504c1c9b..fc5a8d784 100644 --- a/lib/school/ywb/page/service.dart +++ b/lib/school/ywb/page/service.dart @@ -66,7 +66,7 @@ class _YwbServiceListPageState extends State { isLoading = false; }); } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); if (!mounted) return; setState(() { isLoading = false; diff --git a/lib/session/sso.dart b/lib/session/sso.dart index d91571d5d..21cf260ce 100644 --- a/lib/session/sso.dart +++ b/lib/session/sso.dart @@ -73,7 +73,6 @@ class SsoSession { this.onError, }); - /// - User try to log in actively on a login page. Future loginLocked(Credentials credentials) async { return await _loginLock.synchronized(() async { diff --git a/lib/timetable/i18n.dart b/lib/timetable/i18n.dart index 705447162..2b371c6ec 100644 --- a/lib/timetable/i18n.dart +++ b/lib/timetable/i18n.dart @@ -318,7 +318,6 @@ class _FreeTip { String get findNearestDayWithClass => "$ns.findNearestDayWithClass".tr(); } - class _Settings { const _Settings(); diff --git a/lib/update/utils.dart b/lib/update/utils.dart index 3b9e671a0..ad8c4ef72 100644 --- a/lib/update/utils.dart +++ b/lib/update/utils.dart @@ -38,7 +38,7 @@ Future checkAppUpdate({ ); } } catch (error, stackTrace) { - handleRequestError(context, error, stackTrace); + handleRequestError(error, stackTrace); } } diff --git a/lib/utils/error.dart b/lib/utils/error.dart index dd82f0054..06d476ae1 100644 --- a/lib/utils/error.dart +++ b/lib/utils/error.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:sit/credentials/error.dart'; import 'package:sit/design/adaptive/dialog.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/login/i18n.dart'; void debugPrintError(Object? error, [StackTrace? stackTrace]) { @@ -30,10 +31,11 @@ void debugPrintError(Object? error, [StackTrace? stackTrace]) { const _i18n = CommonLoginI18n(); -Future handleRequestError(BuildContext context, Object? error, [StackTrace? stackTrace]) async { +Future handleRequestError(Object? error, [StackTrace? stackTrace]) async { debugPrintError(error, stackTrace); + final context = $key.currentContext; if (error is CredentialsException) { - if (!context.mounted) return; + if (context == null || context.mounted) return; await context.showTip( serious: true, title: _i18n.failedWarn, From 8ca1cfe9eb858e70418535345fdcdc31d852d8ad Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 12:32:50 +0800 Subject: [PATCH 120/458] [expense] [statistics] bar chart tooltip --- assets/l10n/zh-Hans.yaml | 4 +- .../expense_records/entity/statistics.dart | 69 ++++++++------ .../expense_records/widget/chart/bar.dart | 54 ++++++++++- .../expense_records/widget/chart/pie.dart | 95 ++++++++++++------- 4 files changed, 153 insertions(+), 69 deletions(-) diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index 6a32da086..4ddc35cba 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -524,8 +524,8 @@ networkTool: campusNetworkConnected: 已连接到校园网 campusNetworkNotConnected: 未连接校园网 campusNetworkConnectedByVpn: 已通过 VPN 连接到校园网 - studentRegAvailable: 教务系统可以访问 - studentRegUnavailable: 教务系统不可访问 + studentRegAvailable: 可以访问教务系统 + studentRegUnavailable: 无法访问教务系统 ugRegAvailableTip: | 你可以从教务系统中导入课程表,并查看考试安排和考试成绩(包括绩点) ugRegUnavailableTip: | diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index af5d7b26f..2715aea5f 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:sit/lifecycle.dart'; import 'package:sit/utils/date.dart'; import 'local.dart'; @@ -33,7 +34,7 @@ enum StatisticsMode { final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); final startTime2Records = ym2records.entries .map((entry) => - (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) + (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) .toList(); startTime2Records.sortBy((r) => r.start); return startTime2Records; @@ -47,7 +48,7 @@ enum StatisticsMode { case StatisticsMode.year: final ym2records = records.groupListsBy((r) => r.timestamp.year); final startTime2Records = - ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); startTime2Records.sortBy((r) => r.start); return startTime2Records; } @@ -58,31 +59,35 @@ enum StatisticsMode { DateTime? endLimit, }) { var end = switch (this) { - StatisticsMode.day => start.copyWith( - day: start.day, - hour: 23, - minute: 59, - second: 59, - ), - StatisticsMode.week => start.copyWith( - day: start.day + 6, - hour: 23, - minute: 59, - second: 59, - ), - StatisticsMode.month => start.copyWith( - day: daysInMonth(year: start.month, month: start.month), - hour: 23, - minute: 59, - second: 59, - ), - StatisticsMode.year => start.copyWith( - month: 12, - day: daysInMonth(year: start.month, month: start.month), - hour: 23, - minute: 59, - second: 59, - ) + StatisticsMode.day => + start.copyWith( + day: start.day, + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.week => + start.copyWith( + day: start.day + 6, + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.month => + start.copyWith( + day: daysInMonth(year: start.month, month: start.month), + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.year => + start.copyWith( + month: 12, + day: daysInMonth(year: start.month, month: start.month), + hour: 23, + minute: 59, + second: 59, + ) }; if (endLimit != null && endLimit.isBefore(end)) { end = endLimit; @@ -90,5 +95,15 @@ enum StatisticsMode { return end; } + String formatDate(DateTime date) { + final local = $key.currentContext?.locale.toString(); + return switch(this){ + StatisticsMode.day => DateFormat.MMMMd(local).format(date), + StatisticsMode.week => DateFormat.MMMMd(local).format(date), + StatisticsMode.month => DateFormat.MMMMd(local).format(date), + StatisticsMode.year => DateFormat.MMMM(local).format(date), + }; + } + String l10nName() => "expenseRecords.statsMode.$name".tr(); } diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 308fcd3d8..8ea2df30a 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -36,7 +36,7 @@ class _ExpenseBarChartState extends State { buildChartHeader().padFromLTRB(16, 8, 0, 8), AspectRatio( aspectRatio: 1.5, - child: AmountChartWidget( + child: ExpenseBarChartWidget( delegate: delegate, ).padSymmetric(v: 12, h: 8), ), @@ -66,20 +66,48 @@ class _ExpenseBarChartState extends State { } } -class AmountChartWidget extends StatelessWidget { +class ExpenseBarChartWidget extends StatefulWidget { final StatisticsDelegate delegate; - const AmountChartWidget({ + const ExpenseBarChartWidget({ super.key, required this.delegate, }); + @override + State createState() => _ExpenseBarChartWidgetState(); +} + +class _ExpenseBarChartWidgetState extends State { + StatisticsDelegate get delegate => widget.delegate; + int touchedIndex = -1; + @override Widget build(BuildContext context) { return BarChart( BarChartData( alignment: BarChartAlignment.center, - barTouchData: BarTouchData(enabled: false), + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + buildToolTip(groupIndex, rod.toY), + context.textTheme.titleMedium ?? const TextStyle(), + ); + }, + getTooltipColor: (group) => context.colorScheme.surfaceVariant, + ), + touchCallback: (FlTouchEvent event, barTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || barTouchResponse == null || barTouchResponse.spot == null) { + touchedIndex = -1; + return; + } + touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; + }); + }, + ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( @@ -134,6 +162,7 @@ class AmountChartWidget extends StatelessWidget { ), groupsSpace: 40, barGroups: delegate.data.mapIndexed((i, records) { + final isTouched = i == touchedIndex; final (:total, :type2Stats) = statisticsTransactionByType(records); var c = 0.0; return BarChartGroupData( @@ -141,6 +170,10 @@ class AmountChartWidget extends StatelessWidget { barRods: [ BarChartRodData( toY: total, + color: Colors.transparent, + borderSide: isTouched + ? BorderSide(color: context.colorScheme.onSurface, width: 1.5) + : const BorderSide(color: Colors.transparent, width: 0), rodStackItems: type2Stats.entries.map((e) { final res = BarChartRodStackItem( c, @@ -157,4 +190,17 @@ class AmountChartWidget extends StatelessWidget { ), ); } + + String buildToolTip(int index, double value) { + if (delegate.mode == StatisticsMode.day) { + return "¥${value}"; + } else { + final records = delegate.data[index]; + final template = records.firstOrNull; + if(template == null) return ""; + final ts = template.timestamp; + return "${delegate.mode.formatDate(ts)}\n ¥${value}"; + // return records; + } + } } diff --git a/lib/life/expense_records/widget/chart/pie.dart b/lib/life/expense_records/widget/chart/pie.dart index 9fc7bc287..095f08105 100644 --- a/lib/life/expense_records/widget/chart/pie.dart +++ b/lib/life/expense_records/widget/chart/pie.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rettulf/rettulf.dart'; import '../../entity/local.dart'; @@ -23,10 +24,12 @@ class ExpensePieChart extends StatefulWidget { } class _ExpensePieChartState extends State { - int touchedIndex = -1; StatisticsDelegate get delegate => widget.delegate; + DateTime get start => delegate.start; + StatisticsMode get mode => delegate.mode; + @override Widget build(BuildContext context) { return [ @@ -35,13 +38,66 @@ class _ExpensePieChartState extends State { ).padFromLTRB(16, 8, 0, 0), AspectRatio( aspectRatio: 1.5, - child: buildChart(), + child: ExpensePieChartWidget( + delegate: delegate, + ), ), buildLegends().padAll(8).align(at: Alignment.topLeft), ].column(caa: CrossAxisAlignment.start); } - Widget buildChart() { + Widget buildLegends() { + return delegate.type2Stats.entries + .sortedBy((e) => -e.value.total) + .map((record) { + final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; + final color = type.color.harmonizeWith(context.colorScheme.primary); + return Chip( + avatar: Icon(type.icon, color: color), + labelStyle: TextStyle(color: color), + label: "${type.l10n()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), + ); + }) + .toList() + .wrap(spacing: 4, runSpacing: 4); + } +} + +class ExpensePieChartHeader extends StatelessWidget { + final double total; + + const ExpensePieChartHeader({ + super.key, + required this.total, + }); + + @override + Widget build(BuildContext context) { + return ExpenseChartHeader( + upper: i18n.stats.total, + content: "¥${total.toStringAsFixed(2)}", + ); + } +} + +class ExpensePieChartWidget extends ConsumerStatefulWidget { + final StatisticsDelegate delegate; + + const ExpensePieChartWidget({ + super.key, + required this.delegate, + }); + + @override + ConsumerState createState() => _PieChartWidgetState(); +} + +class _PieChartWidgetState extends ConsumerState { + StatisticsDelegate get delegate => widget.delegate; + int touchedIndex = -1; + + @override + Widget build(BuildContext context) { return PieChart( PieChartData( pieTouchData: PieTouchData( @@ -79,39 +135,6 @@ class _ExpensePieChartState extends State { ), ); } - - Widget buildLegends() { - return delegate.type2Stats.entries - .sortedBy((e) => -e.value.total) - .map((record) { - final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; - final color = type.color.harmonizeWith(context.colorScheme.primary); - return Chip( - avatar: Icon(type.icon, color: color), - labelStyle: TextStyle(color: color), - label: "${type.l10n()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), - ); - }) - .toList() - .wrap(spacing: 4, runSpacing: 4); - } -} - -class ExpensePieChartHeader extends StatelessWidget { - final double total; - - const ExpensePieChartHeader({ - super.key, - required this.total, - }); - - @override - Widget build(BuildContext context) { - return ExpenseChartHeader( - upper: i18n.stats.total, - content: "¥${total.toStringAsFixed(2)}", - ); - } } class ExpenseAverageTile extends StatelessWidget { From c52caae545f492fba56c1ac7cd8280adb90e5a54 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 14:43:15 +0800 Subject: [PATCH 121/458] [timetable] [desktop] import timetable from file --- assets/l10n/en.yaml | 2 ++ assets/l10n/zh-Hans.yaml | 2 ++ assets/l10n/zh-Hant.yaml | 2 ++ lib/app.dart | 7 +++--- lib/file_type/handle.dart | 20 +++++++++++++++ lib/file_type/protocol.dart | 27 ++++++++++++++++++++ lib/files/handle.dart | 3 --- lib/settings/page/developer.dart | 2 +- lib/timetable/file_type/timetable.dart | 23 +++++++++++++++++ lib/timetable/i18n.dart | 4 +++ lib/timetable/page/import.dart | 2 +- lib/timetable/page/mine.dart | 18 +++++++++++++- lib/timetable/utils.dart | 34 ++++++++++++++++++++++---- 13 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 lib/file_type/handle.dart create mode 100644 lib/file_type/protocol.dart delete mode 100644 lib/files/handle.dart create mode 100644 lib/timetable/file_type/timetable.dart diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 5da8ecd97..0992ed067 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -17,6 +17,8 @@ timetable: deleteRequestDesc: You will lose this timetable permanently emptyTip: Let's import a timetable! details: Details + addFromFileAction: Add timetable + addFromFileDesc: Confirm to add a timetable from file? edit: name: Name tab: diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index 4ddc35cba..ccfaaa3f6 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -17,6 +17,8 @@ timetable: deleteRequestDesc: 这个课程表将被永久删除 emptyTip: 快去导入一个课程表吧! details: 详情 + addFromFileAction: 添加课程表 + addFromFileDesc: 确定从读取的文件中添加课程表? edit: name: 名称 tab: diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index a8049527f..8f19a3b1f 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -17,6 +17,8 @@ timetable: deleteRequestDesc: 你會失去這個課程表,永久地。 emptyTip: 快去匯入一張新的課程表吧! details: 詳細資料 + addFromFileAction: 新增課程表 + addFromFileDesc: 確認從檔案中新增課程表? edit: name: 名稱 tab: diff --git a/lib/app.dart b/lib/app.dart index d42151309..918929d79 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -10,7 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sit/files.dart'; -import 'package:sit/files/handle.dart'; +import 'package:sit/file_type/handle.dart'; import 'package:sit/lifecycle.dart'; import 'package:sit/qrcode/handle.dart'; import 'package:sit/r.dart'; @@ -154,10 +154,11 @@ class _PostServiceRunnerState extends ConsumerState<_PostServiceRunner> { final navigateCtx = $key.currentContext; if (navigateCtx == null) return; if (!kIsWeb) { - final maybePath = uri.toString(); + final maybePath = Uri.decodeFull(uri.toString()); final isFile = await File(maybePath).exists(); if (isFile) { - await onHandleFilePath(path: maybePath); + if (!navigateCtx.mounted) return; + await onHandleFilePath(context: navigateCtx, path: maybePath); return; } } diff --git a/lib/file_type/handle.dart b/lib/file_type/handle.dart new file mode 100644 index 000000000..8b50515e5 --- /dev/null +++ b/lib/file_type/handle.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:sit/design/adaptive/dialog.dart'; +import 'package:sit/file_type/protocol.dart'; +import 'package:sit/l10n/common.dart'; + +const _i18n = CommonI18n(); + +Future onHandleFilePath({ + required BuildContext context, + required String path, +}) async { + for (final handler in FileTypeHandlerProtocol.all) { + if (handler.matchPath(path)) { + await handler.onHandle(context: context, path: path); + return; + } + } + if(!context.mounted) return; + await context.showTip(title: "Unknown file format", desc: "$path", ok: _i18n.ok); +} diff --git a/lib/file_type/protocol.dart b/lib/file_type/protocol.dart new file mode 100644 index 000000000..cb9cbdd6a --- /dev/null +++ b/lib/file_type/protocol.dart @@ -0,0 +1,27 @@ +import 'package:flutter/cupertino.dart'; +import 'package:sit/timetable/file_type/timetable.dart'; +import 'package:path/path.dart'; + +abstract class FileTypeHandlerProtocol { + const FileTypeHandlerProtocol(); + + bool matchPath(String path); + + Future onHandle({ + required BuildContext context, + required String path, + }); + + static final List all = [ + const TimetableFileType(), + ]; +} + +abstract mixin class FixedExtensionFileTypeHandler implements FileTypeHandlerProtocol { + List get extensions; + + @override + bool matchPath(String path) { + return extensions.contains(extension(path)); + } +} diff --git a/lib/files/handle.dart b/lib/files/handle.dart deleted file mode 100644 index bf5a4e637..000000000 --- a/lib/files/handle.dart +++ /dev/null @@ -1,3 +0,0 @@ -Future onHandleFilePath({ - required String path, -}) async {} diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart index 166ed8ec4..93b286367 100644 --- a/lib/settings/page/developer.dart +++ b/lib/settings/page/developer.dart @@ -148,7 +148,7 @@ class AppLinksTile extends ConsumerWidget { children: appLinks .map((uri) => ListTile( title: context.formatYmdhmsNum(uri.ts).text(), - subtitle: uri.uri.toString().text(), + subtitle: Uri.decodeFull(uri.uri.toString()).text(), )) .toList(), ); diff --git a/lib/timetable/file_type/timetable.dart b/lib/timetable/file_type/timetable.dart new file mode 100644 index 000000000..bd8819683 --- /dev/null +++ b/lib/timetable/file_type/timetable.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; +import 'package:sit/file_type/protocol.dart'; +import 'package:sit/timetable/utils.dart'; + +import '../page/mine.dart'; + +class TimetableFileType with FixedExtensionFileTypeHandler implements FileTypeHandlerProtocol { + const TimetableFileType(); + + @override + List get extensions => const [".timetable"]; + + @override + Future onHandle({ + required BuildContext context, + required String path, + }) async { + final timetable = await readTimetableFromFileWithPrompt(context, path); + if (timetable == null) return; + if (!context.mounted) return; + await onTimetableFromFile(context: context, timetable: timetable); + } +} diff --git a/lib/timetable/i18n.dart b/lib/timetable/i18n.dart index 2b371c6ec..7627c1f17 100644 --- a/lib/timetable/i18n.dart +++ b/lib/timetable/i18n.dart @@ -61,6 +61,10 @@ class _Mine { String get emptyTip => "$ns.emptyTip".tr(); String get details => "$ns.details".tr(); + + String get addFromFileAction => "$ns.addFromFileAction".tr(); + + String get addFromFileDesc => "$ns.addFromFileDesc".tr(); } class _P13n { diff --git a/lib/timetable/page/import.dart b/lib/timetable/page/import.dart index 4c14baa01..ccc198326 100644 --- a/lib/timetable/page/import.dart +++ b/lib/timetable/page/import.dart @@ -71,7 +71,7 @@ class _ImportTimetablePageState extends ConsumerState { } Future importFromFile() async { - final timetable = await readTimetableFromFileWithPrompt(context); + final timetable = await readTimetableFromPickedFileWithPrompt(context); if (timetable == null) return; final id = TimetableInit.storage.timetable.add(timetable); if (!mounted) return; diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart index 7aa4d5c93..ad6d4ff4f 100644 --- a/lib/timetable/page/mine.dart +++ b/lib/timetable/page/mine.dart @@ -83,7 +83,7 @@ class _MyTimetableListPageState extends State { } Future<({int id, SitTimetable timetable})?> importFromFile() async { - final timetable = await readTimetableFromFileWithPrompt(context); + final timetable = await readTimetableFromPickedFileWithPrompt(context); if (timetable == null) return null; final id = TimetableInit.storage.timetable.add(timetable); if (!mounted) return null; @@ -420,3 +420,19 @@ class _TimetableDetailsPageState extends State { ); } } + +Future onTimetableFromFile({ + required BuildContext context, + required SitTimetable timetable, +}) async { + final confirm = await context.showActionRequest( + desc: i18n.mine.addFromFileDesc, + action: i18n.mine.addFromFileAction, + cancel: i18n.cancel, + ); + if (confirm != true) return; + TimetableInit.storage.timetable.add(timetable); + await HapticFeedback.mediumImpact(); + if (!context.mounted) return; + context.push("/timetable/mine"); +} diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart index 711898f14..8f092a42d 100644 --- a/lib/timetable/utils.dart +++ b/lib/timetable/utils.dart @@ -16,6 +16,7 @@ import 'package:sit/school/entity/school.dart'; import 'package:sanitize_filename/sanitize_filename.dart'; import 'package:share_plus/share_plus.dart'; import 'package:sit/school/utils.dart'; +import 'package:sit/utils/error.dart'; import 'package:sit/utils/ical.dart'; import 'package:sit/utils/permission.dart'; import 'package:sit/utils/strings.dart'; @@ -156,15 +157,38 @@ Future readTimetableFromPickedFile() async { return timetable; } -Future readTimetableFromFileWithPrompt(BuildContext context) async { +Future readTimetableFromFile(String path) async { + final file = File(path); + final content = await file.readAsString(); + final json = jsonDecode(content); + final timetable = SitTimetable.fromJson(json); + return timetable; +} + +Future readTimetableFromFileWithPrompt(BuildContext context, String path) async { + try { + final timetable = await readTimetableFromFile(path); + return timetable; + } catch (error, stackTrace) { + debugPrintError(error, stackTrace); + if (!context.mounted) return null; + context.showTip( + title: "Format error", + desc: "The file isn't supported. Please select a timetable file.", + ok: i18n.ok, + ); + return null; + } +} + +Future readTimetableFromPickedFileWithPrompt(BuildContext context) async { try { final timetable = await readTimetableFromPickedFile(); return timetable; - } catch (err, stackTrace) { - debugPrint(err.toString()); - debugPrintStack(stackTrace: stackTrace); + } catch (error, stackTrace) { + debugPrintError(error, stackTrace); if (!context.mounted) return null; - if (err is PlatformException) { + if (error is PlatformException) { await showPermissionDeniedDialog(context: context, permission: Permission.storage); } else { context.showTip( From 7746b37c495a76ce09e56ed05dc8639545ee2f9f Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 16:34:46 +0800 Subject: [PATCH 122/458] [macos] MACOSX_DEPLOYMENT_TARGET = "10.14" --- macos/Podfile | 4 ++-- macos/Podfile.lock | 22 ++++++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/macos/Podfile b/macos/Podfile index 4fccf752a..6feac427c 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -37,7 +37,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) target.build_configurations.each do |config| - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.14' end end end diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 263685786..31ec93fd2 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,8 +4,8 @@ PODS: - audio_session (0.0.1): - FlutterMacOS - connectivity_plus (0.0.1): + - Flutter - FlutterMacOS - - ReachabilitySwift - device_info_plus (0.0.1): - FlutterMacOS - dynamic_color (0.0.2): @@ -26,7 +26,6 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - ReachabilitySwift (5.0.0) - screen_retriever (0.0.1): - FlutterMacOS - share_plus (0.0.1): @@ -54,7 +53,7 @@ PODS: DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) @@ -76,17 +75,13 @@ DEPENDENCIES: - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) -SPEC REPOS: - trunk: - - ReachabilitySwift - EXTERNAL SOURCES: app_links: :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos audio_session: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos dynamic_color: @@ -131,8 +126,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_image_compress_macos: c26c3c13ea0f28ae6dea4e139b3292e7729f99f1 @@ -140,11 +135,10 @@ SPEC CHECKSUMS: irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 mobile_scanner: 54ceceae0c8da2457e26a362a6be5c61154b1829 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 @@ -154,6 +148,6 @@ SPEC CHECKSUMS: wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 -PODFILE CHECKSUM: 3b9a20a0f008a48a736327791f019ae667e7992b +PODFILE CHECKSUM: ce7dbe26c78bfc7ba46736094c1e2d25982870fa COCOAPODS: 1.12.1 From 451ae8658e8f71bf9f5d7c07db5991ec2a5d90a8 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 16:42:13 +0800 Subject: [PATCH 123/458] [workflow] fixed iOS SDK issue --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c39fd4ef8..6984311c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,7 +71,7 @@ jobs: path: build/upload-artifacts/SITLife-${{ steps.get_git_info.outputs.RESOLVED_VERSION }}.apk build_ios: - runs-on: macos-latest + runs-on: macos-13 # macos-latest is macos-12 actually if: github.ref == 'refs/heads/master' steps: From 9b82b693f20ce8f279a0d4ffff7914ae8256e296 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 16:44:27 +0800 Subject: [PATCH 124/458] [ios] support upside down --- ios/Runner/Info.plist | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c0f6b3481..e84d3b4bb 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -73,6 +73,7 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UISupportedInterfaceOrientations~ipad From 07dc5c17e2e83ea77cfb666cf3fbc25b539bfb9d Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 19:05:53 +0800 Subject: [PATCH 125/458] [ios] black background on launch screen --- ios/Podfile.lock | 58 +++++++++++++----- ios/Runner.xcodeproj/project.pbxproj | 18 ++++++ .../LaunchBackground.imageset/Contents.json | 20 ------ .../LaunchImage.imageset/1024x1024.png | Bin 0 -> 30646 bytes .../LaunchImage.imageset/256x256.png | Bin 5283 -> 0 bytes .../LaunchImage.imageset/Contents.json | 2 +- ios/Runner/Base.lproj/LaunchScreen.storyboard | 22 +++---- 7 files changed, 70 insertions(+), 50 deletions(-) delete mode 100644 ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/1024x1024.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/256x256.png diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 25677d84d..fa35bbf6f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,7 +9,7 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter - - ReachabilitySwift + - FlutterMacOS - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.4): @@ -49,6 +49,11 @@ PODS: - fk_user_agent (2.0.0): - Flutter - Flutter (1.0.0) + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder - GoogleDataTransport (9.2.5): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) @@ -85,6 +90,21 @@ PODS: - Flutter - just_audio (0.0.1): - Flutter + - libwebp (1.3.2): + - libwebp/demux (= 1.3.2) + - libwebp/mux (= 1.3.2) + - libwebp/sharpyuv (= 1.3.2) + - libwebp/webp (= 1.3.2) + - libwebp/demux (1.3.2): + - libwebp/webp + - libwebp/mux (1.3.2): + - libwebp/demux + - libwebp/sharpyuv (1.3.2) + - libwebp/webp (1.3.2): + - libwebp/sharpyuv + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) - MLImage (1.0.0-beta4) - MLKitBarcodeScanning (3.0.0): - MLKitCommon (~> 9.0) @@ -123,10 +143,12 @@ PODS: - PromisesObjC (2.3.1) - quick_actions_ios (0.0.1): - Flutter - - ReachabilitySwift (5.0.0) - SDWebImage (5.18.0): - SDWebImage/Core (= 5.18.0) - SDWebImage/Core (5.18.0) + - SDWebImageWebPCoder (0.14.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -155,11 +177,12 @@ DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - app_settings (from `.symlinks/plugins/app_settings/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`) - Flutter (from `Flutter`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - just_audio (from `.symlinks/plugins/just_audio/ios`) @@ -189,14 +212,16 @@ SPEC REPOS: - GoogleUtilities - GoogleUtilitiesComponents - GTMSessionFetcher + - libwebp + - Mantle - MLImage - MLKitBarcodeScanning - MLKitCommon - MLKitVision - nanopb - PromisesObjC - - ReachabilitySwift - SDWebImage + - SDWebImageWebPCoder - SwiftyGif EXTERNAL SOURCES: @@ -209,7 +234,7 @@ EXTERNAL SOURCES: audio_session: :path: ".symlinks/plugins/audio_session/ios" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + :path: ".symlinks/plugins/connectivity_plus/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -218,6 +243,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fk_user_agent/ios" Flutter: :path: Flutter + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" irondash_engine_context: @@ -259,23 +286,26 @@ SPEC CHECKSUMS: add_2_calendar: 5eee66d5a3b99cd5e1487a7e03abd4e3ac4aff11 app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc - audio_session: 4f3e461722055d21515cf3261b64c973c062f345 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 + connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 - image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa + libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 @@ -283,14 +313,14 @@ SPEC CHECKSUMS: mobile_scanner: 38dcd8a49d7d485f632b7de65e4900010187aef2 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 quick_actions_ios: d24571db7345d2e48d094db8d077a015a568002d - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 182830bcddc30cde95fbc60dfe4badc3553d94ba - share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 + SDWebImageWebPCoder: c94f09adbca681822edad9e532ac752db713eabf + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 @@ -298,7 +328,7 @@ SPEC CHECKSUMS: url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 PODFILE CHECKSUM: 2dd2fa37aad0529d8a419ffb4dbab67a5a6dd9b9 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cc4818cdb..fcb0edaf8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -148,6 +148,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 8D031BFA1068FBF3F0D98A42 /* [CP] Embed Pods Frameworks */, + 83F349C89AC1A23266ECA68F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -225,6 +226,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 83F349C89AC1A23266ECA68F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 8D031BFA1068FBF3F0D98A42 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json deleted file mode 100644 index a19a54922..000000000 --- a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/1024x1024.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..67c2ee91e24b0189017a8c136dfa60857dd00f59 GIT binary patch literal 30646 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 - + - + @@ -19,34 +19,26 @@ - - - - - + + - + - - - - - + - - + From c9947e63e4179093d2dcf7f01fc2e2ee7ae2e984 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 19:57:41 +0800 Subject: [PATCH 126/458] [timetable] [macos] register Timetable document type --- macos/Runner/Info.plist | 65 ++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 77c73fd09..77aa9eca1 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -4,6 +4,25 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDocumentTypes + + + CFBundleTypeIconSystemGenerated + 1 + CFBundleTypeName + Timetable document + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + life.mysit.SITLife.timetable + + NSDocumentClass + NSDocument + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile @@ -18,6 +37,17 @@ APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) + CFBundleURLTypes + + + CFBundleURLName + SIT Life URL + CFBundleURLSchemes + + life.mysit + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSHasLocalizedDisplayName @@ -32,16 +62,29 @@ NSApplication UILaunchStoryboardName - CFBundleURLTypes - - - CFBundleURLName - SIT Life URL - CFBundleURLSchemes - - life.mysit - - - + UTExportedTypeDeclarations + + + UTTypeConformsTo + + UTTypeDescription + Timetable document + UTTypeIcons + + UTTypeIdentifier + mysit.life.SITLife.timetable + UTTypeTagSpecification + + public.filename-extension + + timetable + + public.mime-type + + application/timetable + + + + From 31ba2b68171c97e008365581a77cf6e78a6b6a12 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 16 Apr 2024 20:25:07 +0800 Subject: [PATCH 127/458] [network] resort --- lib/file_type/handle.dart | 2 +- .../expense_records/entity/statistics.dart | 60 +++++++++---------- .../expense_records/widget/chart/bar.dart | 2 +- lib/network/page/index.dart | 4 +- lib/network/utils.dart | 2 + .../{quick_button.dart => buttons.dart} | 0 lib/network/{ => widgets}/checker.dart | 1 + .../widgets/{entry.dart => entrance.dart} | 4 +- lib/network/widgets/status.dart | 26 -------- lib/settings/page/index.dart | 4 +- lib/settings/page/proxy.dart | 2 +- lib/timetable/page/import.dart | 2 +- 12 files changed, 41 insertions(+), 68 deletions(-) rename lib/network/widgets/{quick_button.dart => buttons.dart} (100%) rename lib/network/{ => widgets}/checker.dart (99%) rename lib/network/widgets/{entry.dart => entrance.dart} (79%) delete mode 100644 lib/network/widgets/status.dart diff --git a/lib/file_type/handle.dart b/lib/file_type/handle.dart index 8b50515e5..b22265c55 100644 --- a/lib/file_type/handle.dart +++ b/lib/file_type/handle.dart @@ -15,6 +15,6 @@ Future onHandleFilePath({ return; } } - if(!context.mounted) return; + if (!context.mounted) return; await context.showTip(title: "Unknown file format", desc: "$path", ok: _i18n.ok); } diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart index 2715aea5f..76ac0fa3d 100644 --- a/lib/life/expense_records/entity/statistics.dart +++ b/lib/life/expense_records/entity/statistics.dart @@ -34,7 +34,7 @@ enum StatisticsMode { final ym2records = records.groupListsBy((r) => (r.timestamp.year, r.timestamp.week)); final startTime2Records = ym2records.entries .map((entry) => - (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) + (start: getDateOfFirstDayInWeek(year: entry.key.$1, week: entry.key.$2), records: entry.value)) .toList(); startTime2Records.sortBy((r) => r.start); return startTime2Records; @@ -48,7 +48,7 @@ enum StatisticsMode { case StatisticsMode.year: final ym2records = records.groupListsBy((r) => r.timestamp.year); final startTime2Records = - ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); + ym2records.entries.map((entry) => (start: DateTime(entry.key), records: entry.value)).toList(); startTime2Records.sortBy((r) => r.start); return startTime2Records; } @@ -59,35 +59,31 @@ enum StatisticsMode { DateTime? endLimit, }) { var end = switch (this) { - StatisticsMode.day => - start.copyWith( - day: start.day, - hour: 23, - minute: 59, - second: 59, - ), - StatisticsMode.week => - start.copyWith( - day: start.day + 6, - hour: 23, - minute: 59, - second: 59, - ), - StatisticsMode.month => - start.copyWith( - day: daysInMonth(year: start.month, month: start.month), - hour: 23, - minute: 59, - second: 59, - ), - StatisticsMode.year => - start.copyWith( - month: 12, - day: daysInMonth(year: start.month, month: start.month), - hour: 23, - minute: 59, - second: 59, - ) + StatisticsMode.day => start.copyWith( + day: start.day, + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.week => start.copyWith( + day: start.day + 6, + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.month => start.copyWith( + day: daysInMonth(year: start.month, month: start.month), + hour: 23, + minute: 59, + second: 59, + ), + StatisticsMode.year => start.copyWith( + month: 12, + day: daysInMonth(year: start.month, month: start.month), + hour: 23, + minute: 59, + second: 59, + ) }; if (endLimit != null && endLimit.isBefore(end)) { end = endLimit; @@ -97,7 +93,7 @@ enum StatisticsMode { String formatDate(DateTime date) { final local = $key.currentContext?.locale.toString(); - return switch(this){ + return switch (this) { StatisticsMode.day => DateFormat.MMMMd(local).format(date), StatisticsMode.week => DateFormat.MMMMd(local).format(date), StatisticsMode.month => DateFormat.MMMMd(local).format(date), diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index 8ea2df30a..a8f64bd91 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -197,7 +197,7 @@ class _ExpenseBarChartWidgetState extends State { } else { final records = delegate.data[index]; final template = records.firstOrNull; - if(template == null) return ""; + if (template == null) return ""; final ts = template.timestamp; return "${delegate.mode.formatDate(ts)}\n ¥${value}"; // return records; diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 6369bc06b..181e2686d 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -10,7 +10,7 @@ import 'package:sit/design/widgets/card.dart'; import 'package:sit/init.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/network/service/network.dart'; -import 'package:sit/network/widgets/quick_button.dart'; +import 'package:sit/network/widgets/buttons.dart'; import '../utils.dart'; import '../i18n.dart'; @@ -154,7 +154,7 @@ class ConnectivityInfo extends StatelessWidget { Icon( status == null ? Icons.public_off - : status.vpnEnabled == true + : status.vpnEnabled ? Icons.vpn_key : getConnectionTypeIcon(status), size: 120, diff --git a/lib/network/utils.dart b/lib/network/utils.dart index afdfff43c..74bb985f0 100644 --- a/lib/network/utils.dart +++ b/lib/network/utils.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:flutter/material.dart'; enum ConnectivityType { @@ -11,6 +12,7 @@ enum ConnectivityType { other; } +@CopyWith(skipFields: true) class ConnectivityStatus { final ConnectivityType? type; final bool vpnEnabled; diff --git a/lib/network/widgets/quick_button.dart b/lib/network/widgets/buttons.dart similarity index 100% rename from lib/network/widgets/quick_button.dart rename to lib/network/widgets/buttons.dart diff --git a/lib/network/checker.dart b/lib/network/widgets/checker.dart similarity index 99% rename from lib/network/checker.dart rename to lib/network/widgets/checker.dart index 76952c941..00d0b9173 100644 --- a/lib/network/checker.dart +++ b/lib/network/widgets/checker.dart @@ -8,6 +8,7 @@ import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/animation/animated.dart'; import 'package:sit/init.dart'; import 'package:sit/network/utils.dart'; +import 'package:sit/settings/settings.dart'; import 'package:sit/utils/error.dart'; import 'package:rettulf/rettulf.dart'; diff --git a/lib/network/widgets/entry.dart b/lib/network/widgets/entrance.dart similarity index 79% rename from lib/network/widgets/entry.dart rename to lib/network/widgets/entrance.dart index 7590e0ebc..e614fa426 100644 --- a/lib/network/widgets/entry.dart +++ b/lib/network/widgets/entrance.dart @@ -3,8 +3,8 @@ import 'package:rettulf/rettulf.dart'; import 'package:sit/design/widgets/navigation.dart'; import '../i18n.dart'; -class NetworkToolEntryTile extends StatelessWidget { - const NetworkToolEntryTile({super.key}); +class NetworkToolEntranceTile extends StatelessWidget { + const NetworkToolEntranceTile({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/network/widgets/status.dart b/lib/network/widgets/status.dart deleted file mode 100644 index 11850c44b..000000000 --- a/lib/network/widgets/status.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; -import '../service/network.dart'; - -class CampusNetworkStatusInfo extends StatelessWidget { - final CampusNetworkStatus? status; - - const CampusNetworkStatusInfo({super.key, required this.status}); - - @override - Widget build(BuildContext context) { - final style = context.textTheme.bodyLarge; - final status = this.status; - var ip = i18n.unknown; - var studentId = i18n.unknown; - if (status != null) { - ip = status.ip; - studentId = status.studentId ?? i18n.unknown; - } - return [ - "${i18n.credentials.studentId}: $studentId".text(textAlign: TextAlign.center, style: style), - "${i18n.network.ipAddress}: $ip".text(textAlign: TextAlign.center, style: style), - ].column(); - } -} diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart index 88a2e2c1e..f090836d4 100644 --- a/lib/settings/page/index.dart +++ b/lib/settings/page/index.dart @@ -10,7 +10,7 @@ import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/lifecycle.dart'; import 'package:sit/login/i18n.dart'; -import 'package:sit/network/widgets/entry.dart'; +import 'package:sit/network/widgets/entrance.dart'; import 'package:sit/storage/hive/init.dart'; import 'package:sit/init.dart'; import 'package:sit/l10n/extension.dart'; @@ -128,7 +128,7 @@ class _SettingsPageState extends ConsumerState { leading: const Icon(Icons.vpn_key), path: "/settings/proxy", )); - all.add(const NetworkToolEntryTile()); + all.add(const NetworkToolEntranceTile()); } if (loginStatus != LoginStatus.never) { all.add(const ClearCacheTile()); diff --git a/lib/settings/page/proxy.dart b/lib/settings/page/proxy.dart index f3ea31fd0..f9e400ca6 100644 --- a/lib/settings/page/proxy.dart +++ b/lib/settings/page/proxy.dart @@ -9,7 +9,7 @@ import 'package:sit/design/adaptive/editor.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/network/checker.dart'; +import 'package:sit/network/widgets/checker.dart'; import 'package:sit/qrcode/page/view.dart'; import 'package:sit/settings/settings.dart'; import 'package:rettulf/rettulf.dart'; diff --git a/lib/timetable/page/import.dart b/lib/timetable/page/import.dart index ccc198326..3d748d48f 100644 --- a/lib/timetable/page/import.dart +++ b/lib/timetable/page/import.dart @@ -8,7 +8,7 @@ import 'package:sit/credentials/entity/user_type.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/animation/animated.dart'; -import 'package:sit/network/checker.dart'; +import 'package:sit/network/widgets/checker.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/school/entity/school.dart'; import 'package:sit/school/utils.dart'; From f59e74909f268d03eaf676fdb169a5e4bb1dbb66 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 01:16:39 +0800 Subject: [PATCH 128/458] [timetable] hide share timetable on Linux because of lack of implementation --- lib/timetable/page/mine.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart index ad6d4ff4f..74b3fe84b 100644 --- a/lib/timetable/page/mine.dart +++ b/lib/timetable/page/mine.dart @@ -20,6 +20,7 @@ import 'package:sit/timetable/platte.dart'; import 'package:sit/timetable/widgets/course.dart'; import 'package:sit/utils/format.dart'; import 'package:text_scroll/text_scroll.dart'; +import 'package:universal_platform/universal_platform.dart'; import '../entity/platte.dart'; import '../i18n.dart'; @@ -272,14 +273,16 @@ class TimetableCard extends StatelessWidget { } }, ), - EntryAction( - label: i18n.share, - icon: context.icons.share, - type: EntryActionType.share, - action: () async { - await exportTimetableFileAndShare(timetable, context: ctx); - }, - ), + // share_plus: sharing files is not supported on Linux + if (!UniversalPlatform.isLinux) + EntryAction( + label: i18n.share, + icon: context.icons.share, + type: EntryActionType.share, + action: () async { + await exportTimetableFileAndShare(timetable, context: ctx); + }, + ), EntryAction( label: i18n.mine.exportCalendar, icon: context.icons.calendar, From 60871618f19889576bfc40b1c3d49b9f81d7a10e Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 15:27:05 +0800 Subject: [PATCH 129/458] [game] riverpod in GameStorageBox --- lib/game/storage/storage.dart | 18 ++++++++++++++- lib/game/widget/card.dart | 33 ++++++++------------------- lib/main.dart | 30 ++++++++++++------------- lib/utils/hive.dart | 42 +++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/lib/game/storage/storage.dart b/lib/game/storage/storage.dart index 5f40dfe21..98b474f71 100644 --- a/lib/game/storage/storage.dart +++ b/lib/game/storage/storage.dart @@ -14,7 +14,7 @@ class GameStorageBox { final TSave Function(Map json) deserialize; final Map Function(TSave save) serialize; - const GameStorageBox({ + GameStorageBox({ required this.name, required this.version, required this.serialize, @@ -47,6 +47,22 @@ class GameStorageBox { return _box.containsKey("/$name/$version/$slot"); } + late final $saveFamily = _box.providerFamily( + (slot) => "/$name/$version/$slot", + get: (slot) => load(slot: slot), + set: (slot, v) async { + if (v == null) { + await delete(slot: slot); + } else { + await save(v, slot: slot); + } + }, + ); + + late final $saveExistsFamily = _box.existsChangeProviderFamily( + (slot) => "/$name/$version/$slot", + ); + Listenable listen({int slot = 0}) { return _box.listenable(keys: ["/$name/$version/$slot"]); } diff --git a/lib/game/widget/card.dart b/lib/game/widget/card.dart index d0d95a070..10cad5f60 100644 --- a/lib/game/widget/card.dart +++ b/lib/game/widget/card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/design/widgets/app.dart'; @@ -7,7 +8,7 @@ import 'package:sit/design/widgets/app.dart'; import '../storage/storage.dart'; import '../i18n.dart'; -class OfflineGameAppCard extends StatefulWidget { +class OfflineGameAppCard extends ConsumerStatefulWidget { final String name; final String baseRoute; final bool supportHistory; @@ -24,33 +25,17 @@ class OfflineGameAppCard extends StatefulWidget { }); @override - State createState() => _OfflineGameAppCardState(); + ConsumerState createState() => _OfflineGameAppCardState(); } -class _OfflineGameAppCardState extends State { - late final $save = widget.storage?.listen(); - late var hasSave = widget.storage?.exists(); - - @override - void initState() { - super.initState(); - $save?.addListener(onSaveChanged); - } - - @override - void dispose() { - $save?.removeListener(onSaveChanged); - super.dispose(); - } - - void onSaveChanged() { - setState(() { - hasSave = widget.storage?.exists(); - }); - } - +class _OfflineGameAppCardState extends ConsumerState { @override Widget build(BuildContext context) { + final storage = widget.storage; + var hasSave = false; + if (storage != null) { + hasSave |= ref.watch(storage.$saveExistsFamily(0)); + } return AppCard( title: widget.name.text(), leftActions: [ diff --git a/lib/main.dart b/lib/main.dart index 6647a4b2f..28c751402 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -98,21 +98,21 @@ void main() async { await Init.initStorage(); await Init.initModules(); runApp( - EasyLocalization( - supportedLocales: R.supportedLocales, - path: 'assets/l10n', - fallbackLocale: R.defaultLocale, - useFallbackTranslations: true, - assetLoader: _yamlAssetsLoader, - child: ScreenUtilInit( - designSize: const Size(360, 690), - minTextAdapt: true, - splitScreenMode: true, - builder: (context, child) { - return const ProviderScope( - child: MimirApp(), - ); - }, + ProviderScope( + child: EasyLocalization( + supportedLocales: R.supportedLocales, + path: 'assets/l10n', + fallbackLocale: R.defaultLocale, + useFallbackTranslations: true, + assetLoader: _yamlAssetsLoader, + child: ScreenUtilInit( + designSize: const Size(360, 690), + minTextAdapt: true, + splitScreenMode: true, + builder: (context, child) { + return const MimirApp(); + }, + ), ), ), ); diff --git a/lib/utils/hive.dart b/lib/utils/hive.dart index 74d88ce72..425feb73f 100644 --- a/lib/utils/hive.dart +++ b/lib/utils/hive.dart @@ -82,6 +82,25 @@ class BoxChangeNotifier extends ChangeNotifier { } } +class BoxFieldExistsChangeNotifier extends StateNotifier { + final Listenable listenable; + final bool Function() getExists; + + BoxFieldExistsChangeNotifier(super._state, this.listenable, this.getExists) { + listenable.addListener(_refresh); + } + + void _refresh() { + state = getExists(); + } + + @override + void dispose() { + listenable.removeListener(_refresh); + super.dispose(); + } +} + typedef BoxEventFilter = bool Function(BoxEvent event); class BoxChangeStreamNotifier extends ChangeNotifier { @@ -186,6 +205,29 @@ extension BoxProviderX on Box { }); } + AutoDisposeStateNotifierProvider existsChangeProvider( + dynamic key, + ) { + return StateNotifierProvider.autoDispose((ref) { + return BoxFieldExistsChangeNotifier( + containsKey(key), + listenable(keys: [key]), + () => containsKey(key), + ); + }); + } + + AutoDisposeStateNotifierProviderFamily existsChangeProviderFamily( + dynamic Function(Arg arg) keyOf) { + return StateNotifierProvider.autoDispose.family((ref, arg) { + return BoxFieldExistsChangeNotifier( + containsKey(keyOf(arg)), + listenable(keys: [keyOf(arg)]), + () => containsKey(keyOf(arg)), + ); + }); + } + AutoDisposeChangeNotifierProvider streamChangeProvider({ BoxEventFilter? filter, }) { From 59906d0952b96cb5a07a92bbd13dda0c506aa42f Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 15:50:47 +0800 Subject: [PATCH 130/458] [minesweeper] serializable game state 50% --- lib/game/2048/entity/record.dart | 4 ++ lib/game/entity/game_state.dart | 9 +++++ lib/game/minesweeper/entity/board.dart | 2 + lib/game/minesweeper/entity/mode.dart | 18 +++++++-- lib/game/minesweeper/entity/state.dart | 23 +++++++----- lib/game/minesweeper/entity/state.g.dart | 48 +++++++++--------------- 6 files changed, 61 insertions(+), 43 deletions(-) create mode 100644 lib/game/entity/game_state.dart diff --git a/lib/game/2048/entity/record.dart b/lib/game/2048/entity/record.dart index 35b37e6cc..a444435bc 100644 --- a/lib/game/2048/entity/record.dart +++ b/lib/game/2048/entity/record.dart @@ -15,4 +15,8 @@ class Record2048 extends GameRecord { required this.score, required this.maxNumber, }); + + Map toJson() => _$Record2048ToJson(this); + + factory Record2048.fromJson(Map json) => _$Record2048FromJson(json); } diff --git a/lib/game/entity/game_state.dart b/lib/game/entity/game_state.dart new file mode 100644 index 000000000..049f15143 --- /dev/null +++ b/lib/game/entity/game_state.dart @@ -0,0 +1,9 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +@JsonEnum() +enum GameState { + running, + idle, + gameOver, + victory, +} diff --git a/lib/game/minesweeper/entity/board.dart b/lib/game/minesweeper/entity/board.dart index 7fa1d3485..8c2676e72 100644 --- a/lib/game/minesweeper/entity/board.dart +++ b/lib/game/minesweeper/entity/board.dart @@ -1,11 +1,13 @@ import 'package:collection/collection.dart'; import "package:flutter/foundation.dart"; +import 'package:freezed_annotation/freezed_annotation.dart'; import '../manager/logic.dart'; import 'package:logger/logger.dart'; import 'dart:math'; import '../save.dart'; import 'cell.dart'; +@JsonSerializable() class Board { var _mines = -1; final int rows; diff --git a/lib/game/minesweeper/entity/mode.dart b/lib/game/minesweeper/entity/mode.dart index 9e11e79f9..fdbe284f2 100644 --- a/lib/game/minesweeper/entity/mode.dart +++ b/lib/game/minesweeper/entity/mode.dart @@ -5,29 +5,39 @@ class GameMode { final int gameMines; static const defaultRows = 15; static const defaultColumns = 8; - static const easy = GameMode( + static const easy = GameMode._( name: "easy", gameRows: defaultRows, gameColumns: defaultColumns, gameMines: 18, ); - static const normal = GameMode( + static const normal = GameMode._( name: "normal", gameRows: 15, gameColumns: 8, gameMines: 18, ); - static const hard = GameMode( + static const hard = GameMode._( name: "hard", gameRows: 15, gameColumns: 8, gameMines: 18, ); - const GameMode({ + static final name2mode = { + "easy": easy, + "normal": normal, + "hard": hard, + }; + + const GameMode._({ required this.name, required this.gameRows, required this.gameColumns, required this.gameMines, }); + + static String toJson(GameMode mode) => mode.name; + + static GameMode fromJson(String name) => name2mode[name] ?? easy; } diff --git a/lib/game/minesweeper/entity/state.dart b/lib/game/minesweeper/entity/state.dart index 2bb628b74..8116db916 100644 --- a/lib/game/minesweeper/entity/state.dart +++ b/lib/game/minesweeper/entity/state.dart @@ -1,24 +1,29 @@ import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:sit/game/minesweeper/entity/screen.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:sit/game/entity/game_state.dart'; import 'board.dart'; import 'mode.dart'; part "state.g.dart"; +// @JsonSerializable() @CopyWith(skipFields: true) -class GameStates { - final bool gameOver; - final bool goodGame; +class GameStateMinesweeper { + @JsonKey() + final GameState state; + @JsonKey(toJson: GameMode.toJson, fromJson: GameMode.fromJson) final GameMode mode; - final Screen screen; + @JsonKey() final Board board; - const GameStates({ - required this.gameOver, - required this.goodGame, + const GameStateMinesweeper({ + required this.state, required this.mode, - required this.screen, required this.board, }); + // + // Map toJson() => _$GameStateMinesweeperToJson(this); + // + // factory GameStateMinesweeper.fromJson(Map json) => _$GameStateMinesweeperFromJson(json); } diff --git a/lib/game/minesweeper/entity/state.g.dart b/lib/game/minesweeper/entity/state.g.dart index baaa9986f..560c72af2 100644 --- a/lib/game/minesweeper/entity/state.g.dart +++ b/lib/game/minesweeper/entity/state.g.dart @@ -6,27 +6,25 @@ part of 'state.dart'; // CopyWithGenerator // ************************************************************************** -abstract class _$GameStatesCWProxy { +abstract class _$GameStateMinesweeperCWProxy { /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. /// /// Usage /// ```dart - /// GameStates(...).copyWith(id: 12, name: "My name") + /// GameStateMinesweeper(...).copyWith(id: 12, name: "My name") /// ```` - GameStates call({ - bool? gameOver, - bool? goodGame, + GameStateMinesweeper call({ + GameState? state, GameMode? mode, - Screen? screen, Board? board, }); } -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfGameStates.copyWith(...)`. -class _$GameStatesCWProxyImpl implements _$GameStatesCWProxy { - const _$GameStatesCWProxyImpl(this._value); +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfGameStateMinesweeper.copyWith(...)`. +class _$GameStateMinesweeperCWProxyImpl implements _$GameStateMinesweeperCWProxy { + const _$GameStateMinesweeperCWProxyImpl(this._value); - final GameStates _value; + final GameStateMinesweeper _value; @override @@ -34,32 +32,22 @@ class _$GameStatesCWProxyImpl implements _$GameStatesCWProxy { /// /// Usage /// ```dart - /// GameStates(...).copyWith(id: 12, name: "My name") + /// GameStateMinesweeper(...).copyWith(id: 12, name: "My name") /// ```` - GameStates call({ - Object? gameOver = const $CopyWithPlaceholder(), - Object? goodGame = const $CopyWithPlaceholder(), + GameStateMinesweeper call({ + Object? state = const $CopyWithPlaceholder(), Object? mode = const $CopyWithPlaceholder(), - Object? screen = const $CopyWithPlaceholder(), Object? board = const $CopyWithPlaceholder(), }) { - return GameStates( - gameOver: gameOver == const $CopyWithPlaceholder() || gameOver == null - ? _value.gameOver + return GameStateMinesweeper( + state: state == const $CopyWithPlaceholder() || state == null + ? _value.state // ignore: cast_nullable_to_non_nullable - : gameOver as bool, - goodGame: goodGame == const $CopyWithPlaceholder() || goodGame == null - ? _value.goodGame - // ignore: cast_nullable_to_non_nullable - : goodGame as bool, + : state as GameState, mode: mode == const $CopyWithPlaceholder() || mode == null ? _value.mode // ignore: cast_nullable_to_non_nullable : mode as GameMode, - screen: screen == const $CopyWithPlaceholder() || screen == null - ? _value.screen - // ignore: cast_nullable_to_non_nullable - : screen as Screen, board: board == const $CopyWithPlaceholder() || board == null ? _value.board // ignore: cast_nullable_to_non_nullable @@ -68,8 +56,8 @@ class _$GameStatesCWProxyImpl implements _$GameStatesCWProxy { } } -extension $GameStatesCopyWith on GameStates { - /// Returns a callable class that can be used as follows: `instanceOfGameStates.copyWith(...)`. +extension $GameStateMinesweeperCopyWith on GameStateMinesweeper { + /// Returns a callable class that can be used as follows: `instanceOfGameStateMinesweeper.copyWith(...)`. // ignore: library_private_types_in_public_api - _$GameStatesCWProxy get copyWith => _$GameStatesCWProxyImpl(this); + _$GameStateMinesweeperCWProxy get copyWith => _$GameStateMinesweeperCWProxyImpl(this); } From 76d739c51b8cf2c55f0168d525f1eaf90383d8b4 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 19:06:38 +0800 Subject: [PATCH 131/458] [network] in-app proxy awareness --- lib/network/connectivity.dart | 93 +++++++++++++++++++++++++ lib/network/connectivity.g.dart | 57 +++++++++++++++ lib/network/page/index.dart | 8 ++- lib/network/utils.dart | 115 ++++++------------------------- lib/network/widgets/checker.dart | 10 ++- lib/settings/settings.dart | 5 ++ 6 files changed, 188 insertions(+), 100 deletions(-) create mode 100644 lib/network/connectivity.dart create mode 100644 lib/network/connectivity.g.dart diff --git a/lib/network/connectivity.dart b/lib/network/connectivity.dart new file mode 100644 index 000000000..1a72eea65 --- /dev/null +++ b/lib/network/connectivity.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:flutter/material.dart'; + +part "connectivity.g.dart"; + +enum ConnectivityType { + bluetooth, + wifi, + ethernet, + cellular, + other; +} + +@CopyWith(skipFields: true) +class ConnectivityStatus { + final ConnectivityType? type; + final bool vpnEnabled; + + const ConnectivityStatus({ + required this.type, + required this.vpnEnabled, + }); + + const ConnectivityStatus.disconnected({ + required this.vpnEnabled, + }) : type = null; + + const ConnectivityStatus.otherType({ + required this.vpnEnabled, + }) : type = ConnectivityType.other; + + @override + String toString() { + return "$type, vpn:$vpnEnabled"; + } +} + +ConnectivityType? _parseResult(ConnectivityResult? r) { + assert(r != ConnectivityResult.none); + assert(r != ConnectivityResult.vpn); + return switch (r) { + ConnectivityResult.bluetooth => ConnectivityType.bluetooth, + ConnectivityResult.wifi => ConnectivityType.wifi, + ConnectivityResult.ethernet => ConnectivityType.ethernet, + ConnectivityResult.mobile => ConnectivityType.cellular, + ConnectivityResult.none => null, + ConnectivityResult.vpn => null, + ConnectivityResult.other => ConnectivityType.other, + null => null, + }; +} + +Future checkConnectivity() async { + var types = await Connectivity().checkConnectivity(); + if (types.isEmpty || types.first == ConnectivityResult.none) { + return const ConnectivityStatus.disconnected(vpnEnabled: false); + } + var vpnEnabled = types.contains(ConnectivityResult.vpn); + if (types.contains(ConnectivityResult.other)) { + return ConnectivityStatus.otherType(vpnEnabled: vpnEnabled); + } + final type = types.where((t) => t != ConnectivityResult.vpn && t != ConnectivityResult.other).firstOrNull; + return ConnectivityStatus(type: _parseResult(type), vpnEnabled: vpnEnabled); +} + +Stream checkPeriodic({ + required Duration period, + required FutureOr Function() check, +}) async* { + while (true) { + final result = await check(); + debugPrint(result.toString()); + yield result; + await Future.delayed(period); + } +} + +const _type2Icon = { + ConnectivityType.bluetooth: Icons.bluetooth, + ConnectivityType.wifi: Icons.wifi, + ConnectivityType.ethernet: Icons.lan, + ConnectivityType.cellular: Icons.signal_cellular_alt, +}; + +IconData getConnectionTypeIcon(ConnectivityStatus? status, {IconData? fallback}) { + if (status == null) return Icons.wifi_find_outlined; + if (status.vpnEnabled) return Icons.vpn_key; + if (status.type == null) return Icons.public_off; + return _type2Icon[status.type] ?? fallback ?? Icons.signal_wifi_statusbar_null_outlined; +} diff --git a/lib/network/connectivity.g.dart b/lib/network/connectivity.g.dart new file mode 100644 index 000000000..733f0b2e8 --- /dev/null +++ b/lib/network/connectivity.g.dart @@ -0,0 +1,57 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'connectivity.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ConnectivityStatusCWProxy { + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// ConnectivityStatus(...).copyWith(id: 12, name: "My name") + /// ```` + ConnectivityStatus call({ + ConnectivityType? type, + bool? vpnEnabled, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfConnectivityStatus.copyWith(...)`. +class _$ConnectivityStatusCWProxyImpl implements _$ConnectivityStatusCWProxy { + const _$ConnectivityStatusCWProxyImpl(this._value); + + final ConnectivityStatus _value; + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. + /// + /// Usage + /// ```dart + /// ConnectivityStatus(...).copyWith(id: 12, name: "My name") + /// ```` + ConnectivityStatus call({ + Object? type = const $CopyWithPlaceholder(), + Object? vpnEnabled = const $CopyWithPlaceholder(), + }) { + return ConnectivityStatus( + type: type == const $CopyWithPlaceholder() + ? _value.type + // ignore: cast_nullable_to_non_nullable + : type as ConnectivityType?, + vpnEnabled: vpnEnabled == const $CopyWithPlaceholder() || vpnEnabled == null + ? _value.vpnEnabled + // ignore: cast_nullable_to_non_nullable + : vpnEnabled as bool, + ); + } +} + +extension $ConnectivityStatusCopyWith on ConnectivityStatus { + /// Returns a callable class that can be used as follows: `instanceOfConnectivityStatus.copyWith(...)`. + // ignore: library_private_types_in_public_api + _$ConnectivityStatusCWProxy get copyWith => _$ConnectivityStatusCWProxyImpl(this); +} diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart index 181e2686d..212856e3d 100644 --- a/lib/network/page/index.dart +++ b/lib/network/page/index.dart @@ -11,9 +11,10 @@ import 'package:sit/init.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/network/service/network.dart'; import 'package:sit/network/widgets/buttons.dart'; -import '../utils.dart'; +import '../connectivity.dart'; import '../i18n.dart'; +import '../utils.dart'; class NetworkToolPage extends StatefulWidget { const NetworkToolPage({super.key}); @@ -33,8 +34,9 @@ class _NetworkToolPageState extends State { @override void initState() { super.initState(); - connectivityChecker = checkConnectivityPeriodic( + connectivityChecker = checkPeriodic( period: const Duration(milliseconds: 1000), + check: () => checkConnectivityWithProxySettings(schoolNetwork: true), ).listen((status) { if (connectivityStatus != status) { if (!mounted) return; @@ -44,7 +46,7 @@ class _NetworkToolPageState extends State { } }); studentRegChecker = checkPeriodic( - period: const Duration(milliseconds: 5000), + period: const Duration(milliseconds: 8000), check: () async { try { return await Init.ugRegSession.checkConnectivity(); diff --git a/lib/network/utils.dart b/lib/network/utils.dart index 74bb985f0..b8516c724 100644 --- a/lib/network/utils.dart +++ b/lib/network/utils.dart @@ -1,97 +1,24 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:flutter/material.dart'; - -enum ConnectivityType { - bluetooth, - wifi, - ethernet, - cellular, - other; -} - -@CopyWith(skipFields: true) -class ConnectivityStatus { - final ConnectivityType? type; - final bool vpnEnabled; - - const ConnectivityStatus({ - required this.type, - required this.vpnEnabled, - }); - - const ConnectivityStatus.disconnected({ - required this.vpnEnabled, - }) : type = null; - - const ConnectivityStatus.otherType({ - required this.vpnEnabled, - }) : type = ConnectivityType.other; - - @override - String toString() { - return "$type, vpn:$vpnEnabled"; +import 'package:sit/settings/settings.dart'; + +import 'connectivity.dart'; + +Future checkConnectivityWithProxySettings({ + required bool schoolNetwork, +}) async { + final status = await checkConnectivity(); + final proxyEnabled = Settings.proxy.anyEnabled; + if (!proxyEnabled) return status; + + final schoolNetworkProxy = Settings.proxy.hasAnyProxyMode(ProxyMode.schoolOnly); + final globalProxy = Settings.proxy.hasAnyProxyMode(ProxyMode.global); + var vpnEnabled = status.vpnEnabled; + if (!schoolNetwork && globalProxy) { + vpnEnabled |= true; } -} - -ConnectivityType? _parseResult(ConnectivityResult? r) { - assert(r != ConnectivityResult.none); - assert(r != ConnectivityResult.vpn); - return switch (r) { - ConnectivityResult.bluetooth => ConnectivityType.bluetooth, - ConnectivityResult.wifi => ConnectivityType.wifi, - ConnectivityResult.ethernet => ConnectivityType.ethernet, - ConnectivityResult.mobile => ConnectivityType.cellular, - ConnectivityResult.none => null, - ConnectivityResult.vpn => null, - ConnectivityResult.other => ConnectivityType.other, - null => null, - }; -} - -Future checkConnectivity() async { - var types = await Connectivity().checkConnectivity(); - if (types.isEmpty || types.first == ConnectivityResult.none) { - return const ConnectivityStatus.disconnected(vpnEnabled: false); - } - var vpnEnabled = types.contains(ConnectivityResult.vpn); - if (types.contains(ConnectivityResult.other)) { - return ConnectivityStatus.otherType(vpnEnabled: vpnEnabled); - } - final type = types.where((t) => t != ConnectivityResult.vpn && t != ConnectivityResult.other).firstOrNull; - return ConnectivityStatus(type: _parseResult(type), vpnEnabled: vpnEnabled); -} - -Stream checkConnectivityPeriodic({ - required Duration period, -}) { - return checkPeriodic(period: period, check: checkConnectivity); -} - -Stream checkPeriodic({ - required Duration period, - required FutureOr Function() check, -}) async* { - while (true) { - final result = await check(); - debugPrint(result.toString()); - yield result; - await Future.delayed(period); + if (schoolNetwork && (schoolNetworkProxy || globalProxy)) { + vpnEnabled |= true; } -} - -const _type2Icon = { - ConnectivityType.bluetooth: Icons.bluetooth, - ConnectivityType.wifi: Icons.wifi, - ConnectivityType.ethernet: Icons.lan, - ConnectivityType.cellular: Icons.signal_cellular_alt, -}; - -IconData getConnectionTypeIcon(ConnectivityStatus? status, {IconData? fallback}) { - if (status == null) return Icons.wifi_find_outlined; - if (status.vpnEnabled) return Icons.vpn_key; - if (status.type == null) return Icons.public_off; - return _type2Icon[status.type] ?? fallback ?? Icons.signal_wifi_statusbar_null_outlined; + return status.copyWith( + vpnEnabled: vpnEnabled, + ); } diff --git a/lib/network/widgets/checker.dart b/lib/network/widgets/checker.dart index 00d0b9173..2b3b7fdd8 100644 --- a/lib/network/widgets/checker.dart +++ b/lib/network/widgets/checker.dart @@ -7,11 +7,12 @@ import 'package:sit/design/adaptive/foundation.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/animation/animated.dart'; import 'package:sit/init.dart'; -import 'package:sit/network/utils.dart'; -import 'package:sit/settings/settings.dart'; +import 'package:sit/network/connectivity.dart'; import 'package:sit/utils/error.dart'; import 'package:rettulf/rettulf.dart'; +import '../utils.dart'; + enum _Status { none, connecting, @@ -50,7 +51,10 @@ class _ConnectivityCheckerState extends State { @override void initState() { super.initState(); - connectivityChecker = checkConnectivityPeriodic(period: const Duration(milliseconds: 1000)).listen((status) { + connectivityChecker = checkPeriodic( + period: const Duration(milliseconds: 1000), + check: () => checkConnectivityWithProxySettings(schoolNetwork: true), + ).listen((status) { if (connectivityStatus != status) { if (!mounted) return; setState(() { diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 35977c1cc..7991ccc79 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -267,6 +267,11 @@ class _Proxy { all.proxyMode = mode; } + /// return null if their proxy mode are not identical. + bool hasAnyProxyMode(ProxyMode mode) { + return http.proxyMode == mode || https.proxyMode == mode || all.proxyMode == mode; + } + Listenable listenProxyMode() => box.listenable(keys: ProxyType.values.map((type) => _ProxyK.proxyMode(type)).toList()); From bdb0425b6806dcb2aa13cf277113fb325c52850b Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 19:32:49 +0800 Subject: [PATCH 132/458] [timetable] wallpaper without extension --- lib/files.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/files.dart b/lib/files.dart index 442cc9788..e06fafffb 100644 --- a/lib/files.dart +++ b/lib/files.dart @@ -37,7 +37,7 @@ class TimetableFiles { File get screenshotFile => Files.screenshot.subFile("timetable.png"); - File get backgroundFile => Files.user.subFile("timetable", "background.img"); + File get backgroundFile => Files.user.subFile("timetable", "background"); // on MIUI, OpenFile can't open file under `Files.user` Directory get calendarDir => (UniversalPlatform.isAndroid ? Files.cache : Files.user).subDir("timetable", "calendar"); From b8ae768bf62115cb46eba71dc11400cf28a75664 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 19:51:47 +0800 Subject: [PATCH 133/458] [desktop&web] join qq group --- lib/me/index.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/me/index.dart b/lib/me/index.dart index d8fabe5c5..6b4142298 100644 --- a/lib/me/index.dart +++ b/lib/me/index.dart @@ -15,12 +15,15 @@ import 'package:sit/qrcode/handle.dart'; import 'package:sit/settings/dev.dart'; import 'package:sit/utils/error.dart'; import 'package:sit/utils/guard_launch.dart'; +import 'package:universal_platform/universal_platform.dart'; import 'package:url_launcher/url_launcher_string.dart'; import "i18n.dart"; const _qGroupNumber = "917740212"; -const _joinQGroupUri = +const _joinQGroupMobileUri = "mqqapi://card/show_pslcard?src_type=internal&version=1&uin=$_qGroupNumber&card_type=group&source=qrcode"; +const _joinQGroupDesktopUri = + "https://qm.qq.com/cgi-bin/qm/qr?k=9Gn1xo7NfyViy73OP-wVy-Tvzw2pW-fp&authKey=IiyjgIkoBD3I37l/ODvjonS4TwiEaceT4HSp0gxNe3kmicvPdb3opS9lQutKx1DH"; const _wechatUri = "weixin://dl/publicaccount?username=gh_61f7fd217d36"; class MePage extends StatefulWidget { @@ -83,7 +86,11 @@ class _MePageState extends State { trailing: PlatformIconButton( onPressed: () async { try { - await launchUrlString(_joinQGroupUri); + if (UniversalPlatform.isIOS || UniversalPlatform.isAndroid) { + await launchUrlString(_joinQGroupMobileUri); + } else { + await launchUrlString(_joinQGroupDesktopUri, mode: LaunchMode.externalApplication); + } } catch (error, stackTrace) { debugPrintError(error, stackTrace); await Clipboard.setData(const ClipboardData(text: _qGroupNumber)); From 8b60095acb9d66714864f70c7702769a7a645af3 Mon Sep 17 00:00:00 2001 From: Liplum Date: Wed, 17 Apr 2024 20:34:44 +0800 Subject: [PATCH 134/458] [update] skipUpdateFor7days --- lib/settings/settings.dart | 21 +++++++++++++-------- lib/update/i18n.dart | 2 ++ lib/update/utils.dart | 5 +++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 7991ccc79..b1e27aef5 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -21,6 +21,7 @@ class _K { class _UpdateK { static const ns = '/update'; static const skippedVersion = '$ns/skippedVersion'; + static const lastSkipUpdateTime = '$ns/lastSkipUpdateTime'; } // ignore: non_constant_identifier_names @@ -37,25 +38,29 @@ class SettingsImpl { late final theme = _Theme(box); late final proxy = _Proxy(box); - Campus get campus => box.safeGet(_K.campus) ?? Campus.fengxian; + Campus get campus => box.safeGet(_K.campus) ?? Campus.fengxian; - set campus(Campus newV) => box.safePut(_K.campus, newV); + set campus(Campus newV) => box.safePut(_K.campus, newV); late final $campus = box.provider(_K.campus); - bool get focusTimetable => box.safeGet(_K.focusTimetable) ?? false; + bool get focusTimetable => box.safeGet(_K.focusTimetable) ?? false; - set focusTimetable(bool newV) => box.safePut(_K.focusTimetable, newV); + set focusTimetable(bool newV) => box.safePut(_K.focusTimetable, newV); late final $focusTimetable = box.provider(_K.focusTimetable); - String? get lastSignature => box.safeGet(_K.lastSignature); + String? get lastSignature => box.safeGet(_K.lastSignature); - set lastSignature(String? value) => box.safePut(_K.lastSignature, value); + set lastSignature(String? value) => box.safePut(_K.lastSignature, value); - String? get skippedVersion => box.safeGet(_UpdateK.skippedVersion); + String? get skippedVersion => box.safeGet(_UpdateK.skippedVersion); - set skippedVersion(String? newV) => box.safePut(_UpdateK.skippedVersion, newV); + set skippedVersion(String? newV) => box.safePut(_UpdateK.skippedVersion, newV); + + DateTime? get lastSkipUpdateTime => box.safeGet(_UpdateK.lastSkipUpdateTime); + + set lastSkipUpdateTime(DateTime? newV) => box.safePut(_UpdateK.lastSkipUpdateTime, newV); } class _ThemeK { diff --git a/lib/update/i18n.dart b/lib/update/i18n.dart index 65beb28df..ed99f9610 100644 --- a/lib/update/i18n.dart +++ b/lib/update/i18n.dart @@ -20,5 +20,7 @@ class _I18n with CommonI18nMixin { String get skipThisVersion => "$ns.skipThisVersion".tr(); + String get skipUpdateFor7days => "$ns.skipUpdateFor7days".tr(); + String get openAppStore => "$ns.openAppStore".tr(); } diff --git a/lib/update/utils.dart b/lib/update/utils.dart index ad8c4ef72..f2c745eb2 100644 --- a/lib/update/utils.dart +++ b/lib/update/utils.dart @@ -126,10 +126,11 @@ Future _requestInstallOnAppStoreInstead({ ), CupertinoActionSheetAction( onPressed: () { - Settings.skippedVersion = latest.toString(); + // Settings.skippedVersion = latest.toString(); + Settings.lastSkipUpdateTime = DateTime.now(); ctx.pop(false); }, - child: i18n.skipThisVersion.text(), + child: i18n.skipUpdateFor7days.text(), ), ], cancelButton: CupertinoActionSheetAction( From 19a35519de15c3c6cfbbd077701ed7e342eeab43 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 00:09:43 +0800 Subject: [PATCH 135/458] [expense] [statistics] remove trailing zeros in tooltip --- lib/life/expense_records/widget/chart/bar.dart | 5 +++-- lib/utils/format.dart | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/life/expense_records/widget/chart/bar.dart b/lib/life/expense_records/widget/chart/bar.dart index a8f64bd91..c214a2a93 100644 --- a/lib/life/expense_records/widget/chart/bar.dart +++ b/lib/life/expense_records/widget/chart/bar.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/utils/date.dart'; +import 'package:sit/utils/format.dart'; import '../../entity/statistics.dart'; import '../../utils.dart'; @@ -193,13 +194,13 @@ class _ExpenseBarChartWidgetState extends State { String buildToolTip(int index, double value) { if (delegate.mode == StatisticsMode.day) { - return "¥${value}"; + return "¥${formatWithoutTrailingZeros(value)}"; } else { final records = delegate.data[index]; final template = records.firstOrNull; if (template == null) return ""; final ts = template.timestamp; - return "${delegate.mode.formatDate(ts)}\n ¥${value}"; + return "${delegate.mode.formatDate(ts)}\n ¥${formatWithoutTrailingZeros(value)}"; // return records; } } diff --git a/lib/utils/format.dart b/lib/utils/format.dart index b5d15a884..a76c3f76c 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -1,8 +1,11 @@ import 'package:collection/collection.dart'; -String formatWithoutTrailingZeros(double amount) { +String formatWithoutTrailingZeros( + double amount, { + int fractionDigits = 2, +}) { if (amount == 0) return "0"; - final number = amount.toStringAsFixed(2); + final number = amount.toStringAsFixed(fractionDigits); if (number.contains('.')) { int index = number.length - 1; while (index >= 0 && number[index] == '0') { From bd99942edfa7516ad8722ffccce1e4d196e42e0f Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 00:54:23 +0800 Subject: [PATCH 136/458] bump image_picker to 1.1.0 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index c0ba9817e..9758110a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1089,10 +1089,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "1f498d086203360cca099d20ffea2963f48c39ce91bdd8a3b6d4a045786b02c8" + sha256: fe9ee64ccb8d599a5dfb0e21cc6652232c610bcf667af4e79b9eb175cc30a7a5 url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.1.0" image_picker_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fdee733f7..939ef9d4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,7 +86,7 @@ dependencies: open_file: ^3.3.2 url_launcher: ^6.2.6 # Open Android / iOS system image picker - image_picker: ^1.0.8 + image_picker: ^1.1.0 file_picker: ^8.0.0+1 share_plus: ^8.0.3 app_settings: ^5.1.1 From a6305665c35f4c178f742fbe3d0aa4169cb7ce16 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 01:01:36 +0800 Subject: [PATCH 137/458] add copyright in README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 5f69bfb5d..ceb13948e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@ Welcome to contribute SIT Life, please read the [contribution guide](specificati ### License The source codes and configurations are open source under [GPL v3](LICENSE). + +### Copyright + +Copyright©️2023 Liplum Dev. All Rights Reserved. + From 023b8bd007cf50ee54bed2b04b4393a43c9b3d2a Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 03:04:24 +0800 Subject: [PATCH 138/458] [timetable] read timetable file in utf-8 on web --- lib/timetable/utils.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart index 8f092a42d..903f41be9 100644 --- a/lib/timetable/utils.dart +++ b/lib/timetable/utils.dart @@ -204,7 +204,9 @@ Future readTimetableFromPickedFileWithPrompt(BuildContext context Future _readTimetableFi(PlatformFile fi) async { if (kIsWeb) { final bytes = fi.bytes; - return bytes == null ? null : String.fromCharCodes(bytes); + if(bytes == null) return null; + // timetable file should be encoding in utf-8. + return const Utf8Decoder().convert(bytes.toList()); } else { final path = fi.path; if (path == null) return null; From cd60e3963087e192e6133afe78db311564189f0a Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 03:10:46 +0800 Subject: [PATCH 139/458] [web] disable InkSparkle splashFactory on web --- lib/app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app.dart b/lib/app.dart index 918929d79..b3a8569c6 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -80,7 +80,7 @@ class _MimirAppState extends ConsumerState { brightness: origin.brightness, ), visualDensity: VisualDensity.comfortable, - splashFactory: InkSparkle.splashFactory, + splashFactory: kIsWeb ? null : InkSparkle.splashFactory, pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: ZoomPageTransitionsBuilder(), From dc130ad3d604a928d4b320f3bc38cc3aa9a7ed38 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 03:45:51 +0800 Subject: [PATCH 140/458] [web] detect browser and show different icons in about page --- lib/settings/page/about.dart | 27 +++++++++++++++++++++------ pubspec.lock | 24 ++++++++++++++++++++++++ pubspec.yaml | 2 ++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/settings/page/about.dart b/lib/settings/page/about.dart index 9f26a28a4..81f426e6b 100644 --- a/lib/settings/page/about.dart +++ b/lib/settings/page/about.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:simple_icons/simple_icons.dart'; import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/widgets/list_tile.dart'; import 'package:sit/r.dart'; @@ -13,7 +14,7 @@ import 'package:sit/entity/version.dart'; import 'package:sit/update/utils.dart'; import 'package:sit/utils/error.dart'; import 'package:sit/utils/guard_launch.dart'; -import 'package:unicons/unicons.dart'; +import 'package:web_browser_detect/web_browser_detect.dart'; import '../i18n.dart'; class AboutSettingsPage extends StatefulWidget { @@ -107,11 +108,11 @@ class _VersionTileState extends ConsumerState { final version = R.currentVersion; return ListTile( leading: switch (version.platform) { - AppPlatform.iOS || AppPlatform.macOS => const Icon(UniconsLine.apple), - AppPlatform.android => const Icon(Icons.android), - AppPlatform.linux => const Icon(UniconsLine.linux), - AppPlatform.windows => const Icon(UniconsLine.windows), - AppPlatform.web => const Icon(UniconsLine.browser), + AppPlatform.iOS || AppPlatform.macOS => const Icon(SimpleIcons.apple), + AppPlatform.android => const Icon(SimpleIcons.android), + AppPlatform.linux => const Icon(SimpleIcons.linux), + AppPlatform.windows => const Icon(SimpleIcons.windows), + AppPlatform.web => Icon(_getBrowserIcon()), AppPlatform.unknown => const Icon(Icons.device_unknown_outlined), }, title: i18n.about.version.text(), @@ -133,6 +134,20 @@ class _VersionTileState extends ConsumerState { } } +IconData _getBrowserIcon() { + final browser = Browser.detectOrNull(); + if (browser == null) return Icons.web; + return switch (browser.browserAgent) { + BrowserAgent.UnKnown => Icons.web, + BrowserAgent.Chrome => SimpleIcons.googlechrome, + BrowserAgent.Safari => SimpleIcons.safari, + BrowserAgent.Firefox => SimpleIcons.firefoxbrowser, + BrowserAgent.Explorer => SimpleIcons.internetexplorer, + BrowserAgent.Edge => SimpleIcons.microsoftedge, + BrowserAgent.EdgeChromium => SimpleIcons.microsoftedge, + }; +} + class CheckUpdateButton extends StatefulWidget { const CheckUpdateButton({super.key}); diff --git a/pubspec.lock b/pubspec.lock index 9758110a0..89c166ca4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" check_vpn_connection: dependency: "direct main" description: @@ -2082,6 +2090,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" universal_io: dependency: transitive description: @@ -2306,6 +2322,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_browser_detect: + dependency: "direct main" + description: + name: web_browser_detect + sha256: "78ba66860b61a993030788a3a4586fb21cb7d9cef966cfd24faa9ad487c3fd8b" + url: "https://pub.dev" + source: hosted + version: "2.0.3" web_socket_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 939ef9d4e..3f6f46f02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,8 @@ dependencies: file_picker: ^8.0.0+1 share_plus: ^8.0.3 app_settings: ^5.1.1 + web_browser_detect: ^2.0.3 + # Desktop support window_manager: ^0.3.8 win32_registry: ^1.1.3 From 4cbf9b9c64c366ca5ad12200321a3c172611581c Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 04:52:35 +0800 Subject: [PATCH 141/458] [timetable] wallpaper support on web --- lib/app.dart | 40 ++++------ lib/main.dart | 2 + lib/timetable/page/p13n/background.dart | 90 ++++++++++++++++++---- lib/timetable/page/p13n/cell_style.dart | 1 + lib/timetable/widgets/timetable/board.dart | 16 +++- 5 files changed, 108 insertions(+), 41 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index b3a8569c6..46104836d 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -36,29 +36,6 @@ class _MimirAppState extends ConsumerState { ); late final router = buildRouter($routingConfig); - @override - void initState() { - super.initState(); - if (!kIsWeb) { - fitSystemScreenshot.init(); - } - } - - @override - void dispose() { - fitSystemScreenshot.release(); - super.dispose(); - } - - @override - void didChangeDependencies() { - // precache timetable background file - if (Settings.timetable.backgroundImage?.enabled == true) { - precacheImage(FileImage(Files.timetable.backgroundFile), context); - } - super.didChangeDependencies(); - } - @override Widget build(BuildContext context) { final demoMode = ref.watch(Dev.$demoMode); @@ -139,6 +116,9 @@ class _PostServiceRunnerState extends ConsumerState<_PostServiceRunner> { @override void initState() { super.initState(); + if (!kIsWeb) { + fitSystemScreenshot.init(); + } if (!kIsWeb) { Future.delayed(Duration.zero).then((value) async { await checkAppUpdate( @@ -168,9 +148,23 @@ class _PostServiceRunnerState extends ConsumerState<_PostServiceRunner> { }); } + @override + void didChangeDependencies() { + // precache timetable background file + final timetableBk = Settings.timetable.backgroundImage; + if (timetableBk != null && timetableBk.enabled) { + if (kIsWeb) { + precacheImage(NetworkImage(timetableBk.path), context); + } else { + precacheImage(FileImage(Files.timetable.backgroundFile), context); + } + } + super.didChangeDependencies(); + } @override void dispose() { $appLink?.cancel(); + fitSystemScreenshot.release(); super.dispose(); } diff --git a/lib/main.dart b/lib/main.dart index 28c751402..b225e6ca9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sit/files.dart'; import 'package:sit/migration/foundation.dart'; @@ -36,6 +37,7 @@ void main() async { // debugRepaintTextRainbowEnabled = true; // debugPaintSizeEnabled = true; WidgetsFlutterBinding.ensureInitialized(); + GoRouter.optionURLReflectsImperativeAPIs = kDebugMode; final prefs = await SharedPreferences.getInstance(); final lastSize = prefs.getLastWindowSize(); await DesktopInit.init(size: lastSize); diff --git a/lib/timetable/page/p13n/background.dart b/lib/timetable/page/p13n/background.dart index 1d76e20bd..6ebc8b267 100644 --- a/lib/timetable/page/p13n/background.dart +++ b/lib/timetable/page/p13n/background.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/design/adaptive/editor.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/common.dart'; import 'package:sit/files.dart'; @@ -17,6 +19,7 @@ import 'package:sit/widgets/modal_image_view.dart'; import 'package:universal_platform/universal_platform.dart'; import "../../i18n.dart"; +/// Persist changes to storage before route popping class TimetableBackgroundEditor extends StatefulWidget { const TimetableBackgroundEditor({super.key}); @@ -35,7 +38,9 @@ class _TimetableBackgroundEditorState extends State w _TimetableBackgroundEditorState() { final bk = Settings.timetable.backgroundImage; rawPath = bk?.path; - renderImageFile = bk?.path == null ? null : Files.timetable.backgroundFile; + if (!kIsWeb) { + renderImageFile = bk?.path == null ? null : Files.timetable.backgroundFile; + } opacity = bk?.opacity ?? 1.0; repeat = bk?.repeat ?? true; antialias = bk?.antialias ?? true; @@ -71,7 +76,7 @@ class _TimetableBackgroundEditorState extends State w SliverList.list(children: [ buildImage().padH(10), buildToolBar().padV(4), - if (Dev.on && rawPath != null) + if (rawPath != null && (Dev.on || (UniversalPlatform.isDesktop || kIsWeb))) ListTile( title: "Selected image".text(), subtitle: rawPath.text(), @@ -86,6 +91,28 @@ class _TimetableBackgroundEditorState extends State w } Future onSave() async { + if (kIsWeb) { + await onSaveWeb(); + } else { + await onSaveIo(); + } + } + + Future onSaveWeb() async { + final background = buildBackgroundImage(); + if (background == null) { + Settings.timetable.backgroundImage = background; + context.pop(null); + return; + } + final img = NetworkImage(background.path); + Settings.timetable.backgroundImage = background; + await precacheImage(img, context); + if (!mounted) return; + context.pop(background); + } + + Future onSaveIo() async { final background = buildBackgroundImage(); final img = FileImage(Files.timetable.backgroundFile); if (background == null) { @@ -96,19 +123,19 @@ class _TimetableBackgroundEditorState extends State w Settings.timetable.backgroundImage = null; if (!mounted) return; context.pop(null); - } else { - final renderImageFile = this.renderImageFile; - if (renderImageFile == null) return; - Settings.timetable.backgroundImage = background; - if (renderImageFile.path != Files.timetable.backgroundFile.path) { - await copyCompressedImageToTarget(source: renderImageFile, target: Files.timetable.backgroundFile.path); - await img.evict(); - if (!mounted) return; - await precacheImage(img, context); - } + return; + } + final renderImageFile = this.renderImageFile; + if (renderImageFile == null) return; + Settings.timetable.backgroundImage = background; + if (renderImageFile.path != Files.timetable.backgroundFile.path) { + await copyCompressedImageToTarget(source: renderImageFile, target: Files.timetable.backgroundFile.path); + await img.evict(); if (!mounted) return; - context.pop(background); + await precacheImage(img, context); } + if (!mounted) return; + context.pop(background); } Future copyCompressedImageToTarget({ @@ -127,6 +154,26 @@ class _TimetableBackgroundEditorState extends State w } } + Future chooseImage() async { + if (kIsWeb) { + await inputImageUrl(); + } else { + await pickImage(); + } + } + + Future inputImageUrl() async { + final url = await Editor.showStringEditor( + context, + desc: "Image URL", + initial: rawPath ?? "", + ); + if (url == null) return; + setState(() { + rawPath = url; + }); + } + Future pickImage() async { final picker = ImagePicker(); final XFile? fi = await picker.pickImage( @@ -156,7 +203,7 @@ class _TimetableBackgroundEditorState extends State w Widget buildToolBar() { return [ FilledButton.icon( - onPressed: pickImage, + onPressed: chooseImage, icon: Icon(context.icons.create), label: i18n.choose.text(), ), @@ -185,20 +232,31 @@ class _TimetableBackgroundEditorState extends State w Widget buildPreviewBoxContent() { final renderImageFile = this.renderImageFile; final height = context.mediaQuery.size.height / 3; + final rawPath = this.rawPath; + final filterQuality = antialias ? FilterQuality.low : FilterQuality.none; if (renderImageFile != null) { return ModalImageViewer( child: Image.file( renderImageFile, opacity: $opacity, height: height, - filterQuality: antialias ? FilterQuality.low : FilterQuality.none, + filterQuality: filterQuality, + ), + ); + } else if (kIsWeb && rawPath != null) { + return ModalImageViewer( + child: Image.network( + rawPath, + opacity: $opacity, + height: height, + filterQuality: filterQuality, ), ); } else { return LeavingBlank( icon: Icons.add_photo_alternate_outlined, desc: i18n.p13n.background.pickTip, - onIconTap: renderImageFile == null ? pickImage : null, + onIconTap: renderImageFile == null ? chooseImage : null, ).sized(h: height); } } diff --git a/lib/timetable/page/p13n/cell_style.dart b/lib/timetable/page/p13n/cell_style.dart index d70239edf..1ae9d293f 100644 --- a/lib/timetable/page/p13n/cell_style.dart +++ b/lib/timetable/page/p13n/cell_style.dart @@ -9,6 +9,7 @@ import '../../widgets/style.dart'; import '../../i18n.dart'; import 'palette.dart'; +/// Persist changes to storage before route popping class TimetableCellStyleEditor extends StatefulWidget { const TimetableCellStyleEditor({super.key}); diff --git a/lib/timetable/widgets/timetable/board.dart b/lib/timetable/widgets/timetable/board.dart index a9d9260cf..4c35a4745 100644 --- a/lib/timetable/widgets/timetable/board.dart +++ b/lib/timetable/widgets/timetable/board.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/files.dart'; @@ -97,12 +98,23 @@ class _TimetableBackgroundState extends State with SingleTi @override Widget build(BuildContext context) { final bk = widget.background; + final filterQuality = bk.antialias ? FilterQuality.low : FilterQuality.none; + final repeat = bk.repeat ? ImageRepeat.repeat : ImageRepeat.noRepeat; + if (kIsWeb) { + return Image.network( + key: ValueKey(bk.path), + bk.path, + opacity: $opacity, + filterQuality: filterQuality, + repeat: repeat, + ); + } return Image.file( key: ValueKey(bk.path), Files.timetable.backgroundFile, opacity: $opacity, - filterQuality: bk.antialias ? FilterQuality.low : FilterQuality.none, - repeat: bk.repeat ? ImageRepeat.repeat : ImageRepeat.noRepeat, + filterQuality: filterQuality, + repeat: repeat, ); } } From 3176a1d32d39bf8c59bc67000eca74f17c168161 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 05:02:21 +0800 Subject: [PATCH 142/458] [timetable] fixed delete button in wallpaper editor on web --- lib/design/adaptive/multiplatform.dart | 3 ++- lib/timetable/page/p13n/background.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/design/adaptive/multiplatform.dart b/lib/design/adaptive/multiplatform.dart index 0507b29e3..22e79836e 100644 --- a/lib/design/adaptive/multiplatform.dart +++ b/lib/design/adaptive/multiplatform.dart @@ -7,7 +7,8 @@ import 'package:universal_platform/universal_platform.dart'; bool get isCupertino => R.debugCupertino || UniversalPlatform.isIOS || UniversalPlatform.isMacOS; -bool get supportContextMenu => kIsWeb || isCupertino || UniversalPlatform.isDesktop; +bool get supportContextMenu => + kIsWeb || UniversalPlatform.isIOS || UniversalPlatform.isMacOS || UniversalPlatform.isDesktop; extension ShareX on BuildContext { Rect? getSharePositionOrigin() { diff --git a/lib/timetable/page/p13n/background.dart b/lib/timetable/page/p13n/background.dart index 6ebc8b267..972aa732d 100644 --- a/lib/timetable/page/p13n/background.dart +++ b/lib/timetable/page/p13n/background.dart @@ -208,7 +208,7 @@ class _TimetableBackgroundEditorState extends State w label: i18n.choose.text(), ), OutlinedButton.icon( - onPressed: renderImageFile == null + onPressed: (kIsWeb ? rawPath == null : renderImageFile == null) ? null : () { setState(() { From 99de8e0ab31d95061ca494771f52e9ae003a45e0 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 05:06:30 +0800 Subject: [PATCH 143/458] [timetable] fixed wallpaper issues --- lib/timetable/widgets/timetable/board.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/timetable/widgets/timetable/board.dart b/lib/timetable/widgets/timetable/board.dart index 4c35a4745..dc3aeac4a 100644 --- a/lib/timetable/widgets/timetable/board.dart +++ b/lib/timetable/widgets/timetable/board.dart @@ -76,6 +76,10 @@ class _TimetableBackgroundState extends State with SingleTi void initState() { super.initState(); $opacity = AnimationController(vsync: this, value: 0); + $opacity.animateTo( + widget.background.opacity, + duration: Durations.medium1, + ); } @override From 0d1569d6082db384280c5be68d55011adde15000 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 05:37:04 +0800 Subject: [PATCH 144/458] [edu email] open external page on web --- lib/me/edu_email/index.dart | 56 +++++++++++++++++++----------- lib/me/edu_email/page/login.dart | 6 ++-- lib/route.dart | 10 +++--- lib/school/library/page/login.dart | 4 +-- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/lib/me/edu_email/index.dart b/lib/me/edu_email/index.dart index f9e646e54..d2d468c2c 100644 --- a/lib/me/edu_email/index.dart +++ b/lib/me/edu_email/index.dart @@ -1,12 +1,16 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sit/credentials/init.dart'; import 'package:sit/design/widgets/app.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import "i18n.dart"; +const emailUrl = "http://imap.mail.sit.edu.cn"; + class EduEmailAppCard extends ConsumerStatefulWidget { const EduEmailAppCard({super.key}); @@ -22,32 +26,42 @@ class _EduEmailAppCardState extends ConsumerState { return AppCard( title: i18n.title.text(), subtitle: email != null ? SelectableText(email) : null, - leftActions: credentials == null + leftActions: kIsWeb ? [ FilledButton.icon( - onPressed: () { - context.push("/edu-email/login"); + onPressed: () async { + await launchUrlString(emailUrl, mode: LaunchMode.externalApplication); }, - icon: const Icon(Icons.login), - label: i18n.action.login.text(), + icon: const Icon(Icons.open_in_browser), + label: "Open".text(), ), ] - : [ - FilledButton.icon( - onPressed: () { - context.push("/edu-email/inbox"); - }, - icon: const Icon(Icons.inbox), - label: i18n.action.inbox.text(), - ), - OutlinedButton.icon( - onPressed: () { - context.push("/edu-email/outbox"); - }, - icon: const Icon(Icons.outbox), - label: i18n.action.outbox.text(), - ), - ], + : credentials == null + ? [ + FilledButton.icon( + onPressed: () async { + await context.push("/edu-email/login"); + }, + icon: const Icon(Icons.login), + label: i18n.action.login.text(), + ), + ] + : [ + FilledButton.icon( + onPressed: () { + context.push("/edu-email/inbox"); + }, + icon: const Icon(Icons.inbox), + label: i18n.action.inbox.text(), + ), + OutlinedButton.icon( + onPressed: () { + context.push("/edu-email/outbox"); + }, + icon: const Icon(Icons.outbox), + label: i18n.action.outbox.text(), + ), + ], ); } } diff --git a/lib/me/edu_email/page/login.dart b/lib/me/edu_email/page/login.dart index 7dfa70103..bcf8e3e47 100644 --- a/lib/me/edu_email/page/login.dart +++ b/lib/me/edu_email/page/login.dart @@ -1,5 +1,4 @@ import 'package:email_validator/email_validator.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -11,6 +10,8 @@ import 'package:sit/login/utils.dart'; import 'package:sit/login/widgets/forgot_pwd.dart'; import 'package:sit/r.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/settings/dev.dart'; +import 'package:sit/utils/error.dart'; import '../init.dart'; import '../i18n.dart'; @@ -81,7 +82,7 @@ class _EduEmailLoginPageState extends State { controller: $username, textInputAction: TextInputAction.next, autofocus: true, - readOnly: !kDebugMode && initialAccount != null, + readOnly: !Dev.on && initialAccount != null, autocorrect: false, enableSuggestions: false, validator: (username) { @@ -164,6 +165,7 @@ class _EduEmailLoginPageState extends State { setState(() => isLoggingIn = false); context.replace("/edu-email/inbox"); } catch (error, stackTrace) { + debugPrintError(error, stackTrace); if (!mounted) return; setState(() => isLoggingIn = false); if (error is Exception) { diff --git a/lib/route.dart b/lib/route.dart index 94a5fda8a..38d28cbb8 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -307,11 +307,11 @@ final _oaAnnounceRoute = GoRoute( ), ], ); +final _yellowPagesRoute = GoRoute( + path: "/yellow-pages", + builder: (ctx, state) => const YellowPagesListPage(), +); final _eduEmailRoutes = [ - GoRoute( - path: "/yellow-pages", - builder: (ctx, state) => const YellowPagesListPage(), - ), GoRoute( path: "/edu-email/login", builder: (ctx, state) => const EduEmailLoginPage(), @@ -511,6 +511,7 @@ RoutingConfig buildCommonRoutingConfig() { _browserRoute, _expenseRoute, _settingsRoute, + _yellowPagesRoute, ..._toolsRoutes, _class2ndRoute, _oaAnnounceRoute, @@ -544,6 +545,7 @@ RoutingConfig buildTimetableFocusRouter() { _browserRoute, _expenseRoute, _settingsRoute, + _yellowPagesRoute, ..._toolsRoutes, _class2ndRoute, _oaAnnounceRoute, diff --git a/lib/school/library/page/login.dart b/lib/school/library/page/login.dart index 752f3d722..c6c792705 100644 --- a/lib/school/library/page/login.dart +++ b/lib/school/library/page/login.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -10,6 +9,7 @@ import 'package:sit/login/utils.dart'; import 'package:sit/login/widgets/forgot_pwd.dart'; import 'package:rettulf/rettulf.dart'; import 'package:sit/school/library/api.dart'; +import 'package:sit/settings/dev.dart'; import 'package:sit/utils/error.dart'; import '../init.dart'; import '../i18n.dart'; @@ -78,7 +78,7 @@ class _LibraryLoginPageState extends State { controller: $readerId, textInputAction: TextInputAction.next, autofocus: true, - readOnly: !kDebugMode && initialAccount != null, + readOnly: !Dev.on && initialAccount != null, autocorrect: false, enableSuggestions: false, decoration: InputDecoration( From 7e1fe76e6a05d8734738391e0fe33ea7877b2135 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 05:42:19 +0800 Subject: [PATCH 145/458] [edu email] i18n of opening external page on web --- assets/l10n/en.yaml | 1 + assets/l10n/zh-Hans.yaml | 1 + assets/l10n/zh-Hant.yaml | 1 + lib/me/edu_email/i18n.dart | 2 ++ lib/me/edu_email/index.dart | 2 +- 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index 0992ed067..b5a0befee 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -395,6 +395,7 @@ electricity: eduEmail: title: Edu email action: + open: Open login: Login inbox: Inbox outbox: Outbox diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index ccfaaa3f6..0fd3d0451 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -392,6 +392,7 @@ electricity: eduEmail: title: 教育邮箱 action: + open: 打开 login: 登录 inbox: 收件箱 outbox: 发件箱 diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index 8f19a3b1f..2a8669d15 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -392,6 +392,7 @@ electricity: eduEmail: title: Edu 電郵 action: + open: 打開 login: 登入 inbox: 收件匣 outbox: 寄件匣 diff --git a/lib/me/edu_email/i18n.dart b/lib/me/edu_email/i18n.dart index 03cbd88af..110ca2430 100644 --- a/lib/me/edu_email/i18n.dart +++ b/lib/me/edu_email/i18n.dart @@ -30,6 +30,8 @@ class _Action { static const ns = "${_I18n.ns}.action"; + String get open => "$ns.open".tr(); + String get login => "$ns.login".tr(); String get inbox => "$ns.inbox".tr(); diff --git a/lib/me/edu_email/index.dart b/lib/me/edu_email/index.dart index d2d468c2c..1015dadbb 100644 --- a/lib/me/edu_email/index.dart +++ b/lib/me/edu_email/index.dart @@ -33,7 +33,7 @@ class _EduEmailAppCardState extends ConsumerState { await launchUrlString(emailUrl, mode: LaunchMode.externalApplication); }, icon: const Icon(Icons.open_in_browser), - label: "Open".text(), + label: i18n.action.open.text(), ), ] : credentials == null From fcf3728dbf54bc82193ab15fdafd26523cb09ea0 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 06:01:49 +0800 Subject: [PATCH 146/458] [timetable] i18n of wallpaper support on web --- assets/l10n/en.yaml | 4 ++++ assets/l10n/zh-Hans.yaml | 4 ++++ assets/l10n/zh-Hant.yaml | 4 ++++ lib/settings/page/proxy.dart | 4 ++-- lib/timetable/i18n.dart | 8 ++++++++ lib/timetable/page/p13n/background.dart | 17 ++++++++++++++--- 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml index b5a0befee..3656d5463 100644 --- a/assets/l10n/en.yaml +++ b/assets/l10n/en.yaml @@ -152,6 +152,10 @@ timetable: title: Wallpaper opacity: Wallpaper opacity pickTip: Pick your favorite image + selectedImage: Selected image + imageURL: Image URL + invalidURL: Invalid URL + invalidURLDesc: Please enter a URL that begins with http or https repeat: title: Repeat image desc: Repeat image as a pattern to fill wallpaper diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml index 0fd3d0451..55d9cc4da 100644 --- a/assets/l10n/zh-Hans.yaml +++ b/assets/l10n/zh-Hans.yaml @@ -152,6 +152,10 @@ timetable: title: 壁纸 opacity: 背景不透明度 pickTip: 选择你喜欢的图片 + selectedImage: 选择的图片 + imageURL: 图片 URL + invalidURL: 无效的 URL + invalidURLDesc: 请输入以 http 或 https 开头的 URL repeat: title: 重复图像 desc: 重复图像作为填充壁纸的图案 diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml index 2a8669d15..286054dbb 100644 --- a/assets/l10n/zh-Hant.yaml +++ b/assets/l10n/zh-Hant.yaml @@ -153,6 +153,10 @@ timetable: title: 桌布 opacity: 背景不透明度 pickTip: 選取你喜愛的影像 + selectedImage: 選取的影像 + imageURL: 影像 URL + invalidURL: 無效 URL + invalidURLDesc: 請輸入以 http 或 https 開頭的 URL repeat: title: 重複影像 desc: 重複影像作為圖案來填滿桌布 diff --git a/lib/settings/page/proxy.dart b/lib/settings/page/proxy.dart index f9e400ca6..f6248e618 100644 --- a/lib/settings/page/proxy.dart +++ b/lib/settings/page/proxy.dart @@ -301,7 +301,7 @@ class _ProxyProfileEditorPageState extends State { } Widget buildProxyProtocolTile() { - final scheme = uri.scheme; + final scheme = uri.scheme.toLowerCase(); return ListTile( isThreeLine: true, leading: const Icon(Icons.https), @@ -313,7 +313,7 @@ class _ProxyProfileEditorPageState extends State { onSelected: (value) { setState(() { uri = uri.replace( - scheme: protocol, + scheme: protocol.toLowerCase(), ); }); }, diff --git a/lib/timetable/i18n.dart b/lib/timetable/i18n.dart index 7627c1f17..0a678bae0 100644 --- a/lib/timetable/i18n.dart +++ b/lib/timetable/i18n.dart @@ -161,6 +161,14 @@ class _Background { String get pickTip => "$ns.pickTip".tr(); + String get selectedImage => "$ns.selectedImage".tr(); + + String get imageURL => "$ns.imageURL".tr(); + + String get invalidURL => "$ns.invalidURL".tr(); + + String get invalidURLDesc => "$ns.invalidURLDesc".tr(); + String get opacity => "$ns.opacity".tr(); String get repeat => "$ns.repeat.title".tr(); diff --git a/lib/timetable/page/p13n/background.dart b/lib/timetable/page/p13n/background.dart index 972aa732d..0a6c287cb 100644 --- a/lib/timetable/page/p13n/background.dart +++ b/lib/timetable/page/p13n/background.dart @@ -8,6 +8,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:rettulf/rettulf.dart'; +import 'package:sit/design/adaptive/dialog.dart'; import 'package:sit/design/adaptive/editor.dart'; import 'package:sit/design/adaptive/multiplatform.dart'; import 'package:sit/design/widgets/common.dart'; @@ -78,7 +79,7 @@ class _TimetableBackgroundEditorState extends State w buildToolBar().padV(4), if (rawPath != null && (Dev.on || (UniversalPlatform.isDesktop || kIsWeb))) ListTile( - title: "Selected image".text(), + title: i18n.p13n.background.selectedImage.text(), subtitle: rawPath.text(), ), buildOpacity(), @@ -165,12 +166,22 @@ class _TimetableBackgroundEditorState extends State w Future inputImageUrl() async { final url = await Editor.showStringEditor( context, - desc: "Image URL", + desc: i18n.p13n.background.imageURL, initial: rawPath ?? "", ); if (url == null) return; + final uri = Uri.tryParse(url); + if (!mounted) return; + if (uri == null || !uri.isScheme("http") && !uri.isScheme("https")) { + await context.showTip( + title: i18n.p13n.background.invalidURL, + desc: i18n.p13n.background.invalidURLDesc, + ok: i18n.ok, + ); + return; + } setState(() { - rawPath = url; + rawPath = uri.toString(); }); } From 295a5581e34fbd76592eb52e3ac9aa5ccea56163 Mon Sep 17 00:00:00 2001 From: Liplum Date: Thu, 18 Apr 2024 06:29:16 +0800 Subject: [PATCH 147/458] [web] icons --- specifications/CONTRIBUTION_GUIDE.md | 4 +++ web/android-chrome-192x192.png | Bin 0 -> 4445 bytes web/android-chrome-512x512.png | Bin 0 -> 15312 bytes web/apple-touch-icon.png | Bin 0 -> 1953 bytes web/browserconfig.xml | 9 +++++ web/favicon-16x16.png | Bin 0 -> 265 bytes web/favicon-32x32.png | Bin 0 -> 496 bytes web/favicon.ico | Bin 0 -> 15086 bytes web/favicon.png | Bin 917 -> 0 bytes web/icons/Icon-192.png | Bin 5292 -> 0 bytes web/icons/Icon-512.png | Bin 8252 -> 0 bytes web/icons/Icon-maskable-192.png | Bin 5594 -> 0 bytes web/icons/Icon-maskable-512.png | Bin 20998 -> 0 bytes web/index.html | 15 +++++--- web/manifest.json | 38 +++++++-------------- web/mstile-150x150.png | Bin 0 -> 958 bytes web/safari-pinned-tab.svg | 49 +++++++++++++++++++++++++++ 17 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 web/android-chrome-192x192.png create mode 100644 web/android-chrome-512x512.png create mode 100644 web/apple-touch-icon.png create mode 100644 web/browserconfig.xml create mode 100644 web/favicon-16x16.png create mode 100644 web/favicon-32x32.png create mode 100644 web/favicon.ico delete mode 100644 web/favicon.png delete mode 100644 web/icons/Icon-192.png delete mode 100644 web/icons/Icon-512.png delete mode 100644 web/icons/Icon-maskable-192.png delete mode 100644 web/icons/Icon-maskable-512.png create mode 100644 web/mstile-150x150.png create mode 100644 web/safari-pinned-tab.svg diff --git a/specifications/CONTRIBUTION_GUIDE.md b/specifications/CONTRIBUTION_GUIDE.md index bd89c0f4a..4f156ea7a 100644 --- a/specifications/CONTRIBUTION_GUIDE.md +++ b/specifications/CONTRIBUTION_GUIDE.md @@ -95,3 +95,7 @@ python ./tool/main.py Build tool will locate the project automatically, so you can run the [main.py](/tool/main.py) anywhere. + + +### Web icon +Generated on https://realfavicongenerator.net/ diff --git a/web/android-chrome-192x192.png b/web/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..30781bbaa43c2f9e9628695251da5e0f2340f556 GIT binary patch literal 4445 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 Y4es+nyV^<_XC(jw^ex>wZS2GU0fx{k)&Kwi literal 0 HcmV?d00001 diff --git a/web/android-chrome-512x512.png b/web/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..e0bd3c8abfff7cadfcb66cc5e7cacd818d8926a6 GIT binary patch literal 15312 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 literal 0 HcmV?d00001 diff --git a/web/apple-touch-icon.png b/web/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..539c7ccf7b14236ef253e25811e8061d82529045 GIT binary patch literal 1953 zcmV;S2VVGzP)Px#jZjQfMgRZ*Lb1C@guzCTz(S_KLaMz%wYovMxk!J( zL!rX@{ryLXz<|iqMVP)spuI+n!9<(FMU}!rt-bE{`S$z#TEE3gy1t~@;ZnT8MY+6e z#mr;D%B;)ccFNF>(B9qc^K-Y*z~AcT^Y&Yh$abpHu-@Zdp2>B?&YZvA(ChDCvC7ck z?{1yWP^!d=vDlf8$p!!b22V*uK~#9!?VM|mqA(PO9mTmIGhP5;K&eGQ5b^*2yrn=Z zGDSz{bQ6;EWOu)qG*1q1uaq5)5ClOG1VIo4K@bE%5CnlkH$zh&ma-Wij>U1eF$d(F z`Kzg>eJqwyJ{)KFrIB7HL$MD17a=`Ag~O0`UR%<$%^;jD?@Btd29XZ;N_szte313E zG{H*$pG=>u0gfK&Fory0x1w9+GiUlheLC8I%Ml8l%m z1VNDQPz;ND-;g>%5EvEPUJlpnu82CMYL|jm=4?IR_ifvDOV%adC2!Zfmb@@lt0_a% zw6B`ah6!UnpE1Vc@k!-06gtM%dstdHf7a5I+PNkDepVNeEGPqUSNfGpuXv@W9qUHB zC9Q6QQ#xNuuZ8rtw9>0u`fK{7-Kq3g)d2|!y3%(lEy?ti^?#5yo#4qz^Fnvh@jv20 zgQqJkN-eCr(u%f{rV|8kneKK(HCstNbArETy06lT(qYBVSGt?&pOxOInpPZjnLaBz z93jr1GJUJkvsBf_Ec<<>f3fyxReB$(+p<^E#x7~t#T@6?Y-CWP@Z5}+(x>rz!+8)p zZM|GX@452(X&9#iI&C?>596IgDSf&)?+0>nq^IMS^gv!&S<%ILD3KnS`?j=_QA)Qg z-~l^qODovWS$P_rPSM6vCk^hLpX)9IyMZBXY4F7_6|GZP>9M+DI^U}qOiSsC>KOvR zNHI@}x;U4v0Ny!MRlOyjU!0*Q#Ivg4<@0)9R7snv(g9qKyxC+_r-iU{OFmXL)#h`) zH=4~N*tun+YyzP38YX@2J7>Q&U(fc-p^zTGG4Hbil~d#;eoEv8>t?R!eq{470_q3RZ5n+az=k(g!c zek)c@xBgicthAYD6)X=n%jL4MmJO!a>$B2U#OEydGioMXF_|Qi%~tmEtTfq+St|+E zNKeYYiKM$^-#4YherY-vGXVJBB3%k(*8}fl`i68W1||5*lIcQ7%L+p7NZWnVRwRa{ zNe(Z~sH9~>z#VC`S2|2Y4+oEQCB3Y~7n>VB?P2M>6+7_lvQY~&@%Tn)AC}%MMDE~U zyKw1}{Bdc^?~&Gb3sDXD+i=FmN7H&i0HwpDs0Dna^-`KqHhwg3JpVpvuSYr)%`SG} z$5#5TYMcEdZTwkj=R**T)JRw2#^?#@gIt-qQ1#EyKA5UY%I`5ZN^Zu}@8nP|bRdf&X7LGd|BHj;sq z6T?e~g;vx17_)R2-S}2ZCzOW$rBD&+GJIw8)t~;y`1tiY!b_oj92L9~p}chs@-fE3 zSu^0BM-FdZ-VdK7Xx^Ix*FsDJT>1#OLa;o3A7lzzi&l@RIRGwtiOcR*RH%qFDfn}A zAUjHZy@Fk^FcjRLh^HLL6+-Y@Q))XZBF&v)BmK%*D~NP>C?Sius|s zNy#{XN80qbvnw0#bjy5#Fi(@i;oz0xrFj6pr*%Z3CCLk8!?UkBUX!%gUl_k9;=3IW zd@W5}e%aOgG1Z2S{qq*@s#y+-F82u=ao0KY-~J%ob_Slot)Nk|f3_UI{XzQT96)D7cbc32TgjwiJTE;y zUdD}3k+U6JvbFFN(laJKPQ$xOLqqWODA_XSwUPC- ztjAlg7y0M3r&Dy^#FSK74o@ + + + + + #603cba + + + diff --git a/web/favicon-16x16.png b/web/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..1e04d7ae5bcfbbaf4adca3a7f39c1b00bb013b69 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~%K)DcSD(zn zwymd4duDqy9yIQmWLViF)ba4!km|EN4$km|0;}Rb3 zQf&40zrES=g{c=FKKXXwvC6KeE%$ykEH_b4D?8}k`1UxDn2&zL$?m6%eOV3p5~l5u zKcLgP*L}^xGMSm0Q?s=nY!PEx5vbQL9q{+3|8>?a&zQB>oa@m6TE*b$>gTe~DWM4f Dy?SYu literal 0 HcmV?d00001 diff --git a/web/favicon-32x32.png b/web/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e6c0a0e8cd9611917dc8ced1b3d9f9fa79a0fe GIT binary patch literal 496 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 diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100644 index b749bfef07473333cf1dd31e9eed89862a5d52aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 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 diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48dff1169879ba46840804b412fe02fefd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8252 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 diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e525556d5d89141648c724331630325d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20998 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 MPgg&ebxsLQ025IeI{*Lx diff --git a/web/index.html b/web/index.html index 385657a4d..73c1f5c03 100644 --- a/web/index.html +++ b/web/index.html @@ -18,18 +18,25 @@ - + - - + + + + + + + + + - mimir + 小应生活