From a91d2e7fe342d2bd6b1cda37403ae69fc61d1051 Mon Sep 17 00:00:00 2001 From: Liplum Date: Tue, 30 Apr 2024 14:42:49 +0800 Subject: [PATCH] [timetable] handle QR code of Timetable patch --- lib/timetable/entity/loc.dart | 7 +- lib/timetable/entity/platte.dart | 8 + lib/timetable/page/mine.dart | 34 ++-- lib/timetable/page/p13n/palette.dart | 39 +++-- lib/timetable/page/patch/patch.dart | 29 ++++ lib/timetable/page/patch/qrcode.dart | 182 +++++++++++++++++++-- lib/timetable/widgets/patch/patch_set.dart | 57 ++++--- lib/timetable/widgets/patch/shared.dart | 60 ++++--- 8 files changed, 328 insertions(+), 88 deletions(-) diff --git a/lib/timetable/entity/loc.dart b/lib/timetable/entity/loc.dart index 116840f6d..c2ff3a7c1 100644 --- a/lib/timetable/entity/loc.dart +++ b/lib/timetable/entity/loc.dart @@ -73,8 +73,9 @@ class TimetableDayLoc { static TimetableDayLoc deserialize(ByteReader reader) { final mode = TimetableDayLocMode.values[reader.uint8()]; switch (mode) { - case TimetableDayLocMode.pos :return TimetableDayLoc.pos(TimetablePos.deserialize(reader)); - case TimetableDayLocMode.date : + case TimetableDayLocMode.pos: + return TimetableDayLoc.pos(TimetablePos.deserialize(reader)); + case TimetableDayLocMode.date: final packed = reader.uint16(); return TimetableDayLoc.byDate(_unpackYear(packed), _unpackMonth(packed), _unpackDay(packed)); } @@ -133,7 +134,7 @@ int _packDate(DateTime date) { } int _unpackYear(int packedDate) { - return (packedDate >> 9) & 0x1FFF; // Mask to get year bits and add 2000 + return ((packedDate >> 9) & 0x1FFF) + 2000; // Mask to get year bits and add 2000 } int _unpackMonth(int packedDate) { diff --git a/lib/timetable/entity/platte.dart b/lib/timetable/entity/platte.dart index 1d0ea744e..13efc9d96 100644 --- a/lib/timetable/entity/platte.dart +++ b/lib/timetable/entity/platte.dart @@ -108,6 +108,14 @@ class TimetablePalette { } } +extension TimetablePaletteX on TimetablePalette { + TimetablePalette markModified() { + return copyWith( + lastModified: DateTime.now(), + ); + } +} + class BuiltinTimetablePalette implements TimetablePalette { final int id; final String key; diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart index 59ad99f77..53eb2afe3 100644 --- a/lib/timetable/page/mine.dart +++ b/lib/timetable/page/mine.dart @@ -211,9 +211,6 @@ class TimetableCard extends StatelessWidget { @override Widget build(BuildContext context) { - final year = '${timetable.schoolYear}–${timetable.schoolYear + 1}'; - final semester = timetable.semester.l10n(); - return EntryCard( title: timetable.name, selected: selected, @@ -347,13 +344,7 @@ class TimetableCard extends StatelessWidget { ); }, itemBuilder: (ctx) { - final textTheme = ctx.textTheme; - return [ - timetable.name.text(style: textTheme.titleLarge), - "$year, $semester".text(style: textTheme.titleMedium), - if (timetable.signature.isNotEmpty) timetable.signature.text(style: textTheme.bodyMedium), - "${i18n.startWith} ${ctx.formatYmdText(timetable.startDate)}".text(style: textTheme.bodyMedium), - ].column(caa: CrossAxisAlignment.start); + return TimetableInfo(timetable: timetable); }, ); } @@ -374,6 +365,29 @@ class TimetableCard extends StatelessWidget { } } +class TimetableInfo extends StatelessWidget { + final SitTimetable timetable; + + const TimetableInfo({ + super.key, + required this.timetable, + }); + + @override + Widget build(BuildContext context) { + final textTheme = context.textTheme; + final year = '${timetable.schoolYear}–${timetable.schoolYear + 1}'; + final semester = timetable.semester.l10n(); + + return [ + timetable.name.text(style: textTheme.titleLarge), + "$year, $semester".text(style: textTheme.titleMedium), + if (timetable.signature.isNotEmpty) timetable.signature.text(style: textTheme.bodyMedium), + "${i18n.startWith} ${context.formatYmdText(timetable.startDate)}".text(style: textTheme.bodyMedium), + ].column(caa: CrossAxisAlignment.start); + } +} + class TimetableDetailsPage extends ConsumerWidget { final int id; final SitTimetable timetable; diff --git a/lib/timetable/page/p13n/palette.dart b/lib/timetable/page/p13n/palette.dart index 826f156e3..0b6d9bdd1 100644 --- a/lib/timetable/page/p13n/palette.dart +++ b/lib/timetable/page/p13n/palette.dart @@ -213,7 +213,7 @@ class PaletteCard extends StatelessWidget { newPalette = newPalette.copyWith( name: newName, colors: List.of(newPalette.colors), - ); + ).markModified(); } TimetableInit.storage.palette[id] = newPalette; } @@ -244,9 +244,8 @@ class PaletteCard extends StatelessWidget { final duplicate = palette.copyWith( name: allocValidFileName(palette.name, all: allPaletteNames), author: palette.author, - lastModified: DateTime.now(), colors: List.of(palette.colors), - ); + ).markModified(); TimetableInit.storage.palette.add(duplicate); onDuplicate?.call(); }, @@ -285,21 +284,35 @@ class PaletteCard extends StatelessWidget { ); }, itemBuilder: (ctx) { - return [ - palette.name.text(style: ctx.textTheme.titleLarge), - if (palette.author.isNotEmpty) - palette.author.text( - style: const TextStyle( - fontStyle: FontStyle.italic, - ), - ), - PaletteColorsPreview(palette.colors), - ].column(caa: CrossAxisAlignment.start); + return PaletteInfo(palette: palette); }, ); } } +class PaletteInfo extends StatelessWidget { + final TimetablePalette palette; + + const PaletteInfo({ + super.key, + required this.palette, + }); + + @override + Widget build(BuildContext context) { + return [ + palette.name.text(style: context.textTheme.titleLarge), + if (palette.author.isNotEmpty) + palette.author.text( + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ), + PaletteColorsPreview(palette.colors), + ].column(caa: CrossAxisAlignment.start); + } +} + class PaletteDetailsPage extends ConsumerWidget { final int id; final TimetablePalette palette; diff --git a/lib/timetable/page/patch/patch.dart b/lib/timetable/page/patch/patch.dart index 8d53f37e9..e1b044a59 100644 --- a/lib/timetable/page/patch/patch.dart +++ b/lib/timetable/page/patch/patch.dart @@ -322,3 +322,32 @@ class _TimetablePatchDraggableState extends State { } } +class ReadonlyTimetablePatchEntryWidget extends StatelessWidget { + final TimetablePatchEntry entry; + final bool enableQrCode; + + const ReadonlyTimetablePatchEntryWidget({ + super.key, + required this.entry, + this.enableQrCode = true, + }); + + @override + Widget build(BuildContext context) { + final entry = this.entry; + return switch (entry) { + TimetablePatchSet() => TimetablePatchSetCard( + patchSet: entry, + enableQrCode: enableQrCode, + ), + TimetablePatch() => TimetablePatchWidget( + leading: Card.filled( + margin: EdgeInsets.zero, + child: Icon(entry.type.icon).padAll(8), + ), + enableQrCode: enableQrCode, + patch: entry, + ), + }; + } +} diff --git a/lib/timetable/page/patch/qrcode.dart b/lib/timetable/page/patch/qrcode.dart index 821e5cb87..6743bdae6 100644 --- a/lib/timetable/page/patch/qrcode.dart +++ b/lib/timetable/page/patch/qrcode.dart @@ -1,30 +1,190 @@ -import 'package:flutter/widgets.dart'; -import 'package:sit/design/adaptive/dialog.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:rettulf/rettulf.dart'; import 'package:sit/design/adaptive/foundation.dart'; +import 'package:sit/design/widgets/card.dart'; +import 'package:sit/timetable/entity/timetable.dart'; +import 'package:sit/timetable/page/patch/patch.dart'; +import 'package:sit/timetable/page/preview.dart'; +import 'package:text_scroll/text_scroll.dart'; import '../../i18n.dart'; import '../../entity/patch.dart'; +import '../../init.dart'; +import '../mine.dart'; Future onTimetablePatchFromQrCode({ required BuildContext context, required TimetablePatchEntry patch, }) async { - // await HapticFeedback.mediumImpact(); - // if (!context.mounted) return; - // context.push("/timetable/p13n/custom"); - context.showSheet((ctx)=>TimetablePatchFromQrCodeSheet()); + await context.showSheet((ctx) => TimetablePatchFromQrCodeSheet(patch: patch)); } -class TimetablePatchFromQrCodeSheet extends StatefulWidget { - const TimetablePatchFromQrCodeSheet({super.key}); +class TimetablePatchFromQrCodeSheet extends ConsumerStatefulWidget { + final TimetablePatchEntry patch; + + const TimetablePatchFromQrCodeSheet({ + super.key, + required this.patch, + }); + + @override + ConsumerState createState() => _TimetablePatchFromQrCodeSheetState(); +} + +class _TimetablePatchFromQrCodeSheetState extends ConsumerState { + @override + Widget build(BuildContext context) { + final storage = TimetableInit.storage.timetable; + final timetables = ref.watch(storage.$rows); + final patch = widget.patch; + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar.medium( + title: patch is TimetablePatchSet + ? [ + const Icon(Icons.dashboard_customize).padOnly(r: 8), + TextScroll(patch.name).expanded(), + ].row() + : i18n.patch.title.text(), + actions: [ + PlatformTextButton( + onPressed: timetables.isEmpty + ? null + : () async { + final timetable = await context.showSheet( + (context) => TimetablePatchUseSheet(patch: patch), + ); + if (timetable == null) return; + if (!context.mounted) return; + context.pop(); + }, + child: i18n.use.text(), + ) + ], + ), + if (patch is TimetablePatchSet) + SliverList.builder( + itemCount: patch.patches.length, + itemBuilder: (ctx, i) { + final p = patch.patches[i]; + return ReadonlyTimetablePatchEntryWidget( + entry: p, + enableQrCode: false, + ); + }, + ) + else + SliverList.list(children: [ + ReadonlyTimetablePatchEntryWidget( + entry: patch, + enableQrCode: false, + ) + ]), + ], + ), + ); + } +} + +class TimetablePatchUseSheet extends ConsumerStatefulWidget { + final TimetablePatchEntry patch; + + const TimetablePatchUseSheet({ + super.key, + required this.patch, + }); @override - State createState() => _TimetablePatchFromQrCodeSheetState(); + ConsumerState createState() => _TimetablePatchUseSheetState(); +} + +class _TimetablePatchUseSheetState extends ConsumerState { + @override + Widget build(BuildContext context) { + final storage = TimetableInit.storage.timetable; + final timetables = ref.watch(storage.$rows); + assert(timetables.isNotEmpty); + final patch = widget.patch; + timetables.sort((a, b) => b.row.lastModified.compareTo(a.row.lastModified)); + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar.medium( + title: i18n.mine.title.text(), + ), + SliverList.builder( + itemCount: timetables.length, + itemBuilder: (ctx, i) { + final (:id, row: timetable) = timetables[i]; + return TimetablePatchReceiverCard( + id: id, + timetable: timetable, + onAdded: () { + final newTimetable = buildTimetable(timetable, patch).markModified(); + storage[id] = newTimetable; + ctx.pop(newTimetable); + }, + onPreview: () async { + await previewTimetable( + context, + timetable: buildTimetable(timetable, patch), + ); + }, + ).padH(6); + }, + ), + ], + ), + ); + } + + SitTimetable buildTimetable(SitTimetable timetable, TimetablePatchEntry patch) { + return timetable.copyWith( + patches: List.of(timetable.patches)..add(patch), + ); + } } -class _TimetablePatchFromQrCodeSheetState extends State { +class TimetablePatchReceiverCard extends StatelessWidget { + final int id; + final SitTimetable timetable; + final VoidCallback? onAdded; + final VoidCallback? onPreview; + + const TimetablePatchReceiverCard({ + super.key, + required this.id, + required this.timetable, + this.onAdded, + this.onPreview, + }); + @override Widget build(BuildContext context) { - return const Placeholder(); + final onAdded = this.onAdded; + final onPreview = this.onPreview; + return [ + TimetableInfo(timetable: timetable), + OverflowBar( + children: [ + [ + if (onAdded != null) + FilledButton( + onPressed: onAdded, + child: i18n.add.text(), + ), + if (onPreview != null) + OutlinedButton( + onPressed: onPreview, + child: i18n.preview.text(), + ), + ].wrap(spacing: 4), + ], + ), + ].column(caa: CrossAxisAlignment.start).padSymmetric(v: 10, h: 15).inOutlinedCard(); } } diff --git a/lib/timetable/widgets/patch/patch_set.dart b/lib/timetable/widgets/patch/patch_set.dart index 1e5941e2b..abddc95ed 100644 --- a/lib/timetable/widgets/patch/patch_set.dart +++ b/lib/timetable/widgets/patch/patch_set.dart @@ -17,19 +17,21 @@ import 'shared.dart'; class TimetablePatchSetCard extends StatelessWidget { final TimetablePatchSet patchSet; final bool selected; - final SitTimetable timetable; + final SitTimetable? timetable; final VoidCallback? onDeleted; final VoidCallback? onUnpacked; final ValueChanged? onChanged; + final bool enableQrCode; const TimetablePatchSetCard({ super.key, required this.patchSet, - required this.timetable, + this.timetable, this.onDeleted, this.selected = false, this.onUnpacked, this.onChanged, + this.enableQrCode = true, }); @override @@ -71,25 +73,27 @@ class TimetablePatchSetCard extends StatelessWidget { Widget buildMoreActions() { final onChanged = this.onChanged; + final timetable = this.timetable; return PullDownMenuButton( itemBuilder: (context) { return [ - PullDownItem( - icon: context.icons.edit, - title: i18n.edit, - onTap: onChanged == null - ? null - : () async { - final newPatchSet = await context.showSheet( - (ctx) => TimetablePatchSetEditorPage( - timetable: timetable, - patchSet: patchSet, - ), - ); - if (newPatchSet == null) return; - onChanged(newPatchSet); - }, - ), + if (timetable != null) + PullDownItem( + icon: context.icons.edit, + title: i18n.edit, + onTap: onChanged == null + ? null + : () async { + final newPatchSet = await context.showSheet( + (ctx) => TimetablePatchSetEditorPage( + timetable: timetable, + patchSet: patchSet, + ), + ); + if (newPatchSet == null) return; + onChanged(newPatchSet); + }, + ), PullDownItem( icon: context.icons.preview, title: i18n.preview, @@ -97,15 +101,14 @@ class TimetablePatchSetCard extends StatelessWidget { await previewTimetable(context, timetable: timetable); }, ), - if (!kIsWeb) - if (Dev.on) - PullDownItem( - icon: context.icons.qrcode, - title: i18n.shareQrCode, - onTap: () async { - shareTimetablePatchQrCode(context, patchSet); - }, - ), + if (!kIsWeb && enableQrCode) + PullDownItem( + icon: context.icons.qrcode, + title: i18n.shareQrCode, + onTap: () async { + shareTimetablePatchQrCode(context, patchSet); + }, + ), if (onUnpacked != null) PullDownItem.delete( icon: Icons.outbox, diff --git a/lib/timetable/widgets/patch/shared.dart b/lib/timetable/widgets/patch/shared.dart index 7e6df11ed..8484cdd95 100644 --- a/lib/timetable/widgets/patch/shared.dart +++ b/lib/timetable/widgets/patch/shared.dart @@ -141,36 +141,39 @@ class TimetableDayLocDateSelectionTile extends ConsumerWidget { class TimetablePatchMenuAction extends StatelessWidget { final TPatch patch; - final SitTimetable timetable; + final SitTimetable? timetable; final ValueChanged? onChanged; + final bool enableQrCode; const TimetablePatchMenuAction({ super.key, required this.patch, - required this.timetable, + this.timetable, this.onChanged, + this.enableQrCode = true, }); @override Widget build(BuildContext context) { + final timetable = this.timetable; return PullDownMenuButton(itemBuilder: (ctx) { return [ - PullDownItem( - icon: context.icons.preview, - title: i18n.preview, - onTap: () async { - await previewTimetable(context, timetable: timetable); - }, - ), - if (!kIsWeb) - if (Dev.on) - PullDownItem( - title: i18n.shareQrCode, - icon: context.icons.qrcode, - onTap: () async { - shareTimetablePatchQrCode(context, patch); - }, - ), + if (timetable != null) + PullDownItem( + icon: context.icons.preview, + title: i18n.preview, + onTap: () async { + await previewTimetable(context, timetable: timetable); + }, + ), + if (!kIsWeb && enableQrCode) + PullDownItem( + title: i18n.shareQrCode, + icon: context.icons.qrcode, + onTap: () async { + shareTimetablePatchQrCode(context, patch); + }, + ), ]; }); } @@ -180,30 +183,39 @@ class TimetablePatchWidget extends StatelessWidge final Widget? leading; final TPatch patch; final bool selected; - final SitTimetable timetable; + final SitTimetable? timetable; final ValueChanged? onChanged; - final FutureOr Function(TPatch old) edit; + final FutureOr Function(TPatch old)? edit; + final bool enableQrCode; const TimetablePatchWidget({ super.key, this.leading, required this.patch, - required this.timetable, + this.timetable, this.onChanged, - required this.edit, + this.edit, this.selected = false, + this.enableQrCode = true, }); @override Widget build(BuildContext context) { final onChanged = this.onChanged; + final timetable = this.timetable; + final edit = this.edit; return ListTile( leading: leading ?? Icon(patch.type.icon), title: patch.type.l10n().text(), subtitle: patch.l10n().text(), selected: selected, - trailing: TimetablePatchMenuAction(patch: patch, timetable: timetable, onChanged: onChanged), - onTap: onChanged == null + trailing: TimetablePatchMenuAction( + patch: patch, + timetable: timetable, + onChanged: onChanged, + enableQrCode: enableQrCode, + ), + onTap: onChanged == null || edit == null ? null : () async { final newPath = await edit(patch);