Skip to content

Commit

Permalink
[timetable] [patch] recommendations
Browse files Browse the repository at this point in the history
  • Loading branch information
liplum committed Sep 1, 2024
1 parent 7618468 commit 766fd56
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 59 deletions.
3 changes: 3 additions & 0 deletions lib/timetable/patch/builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import 'entity/patch.dart';
class BuiltinTimetablePatchSets {
static final vacationShift2024 = BuiltinTimetablePatchSet(
key: "sitVacationShift2024",
recommended: (timetable) {
return timetable.schoolYear == 2024 || timetable.schoolYear == 2023;
},
patches: [
// New Year's Day
TimetableRemoveDayPatch.oneDay(loc: TimetableDayLoc.byDate(2024, 1, 1)),
Expand Down
2 changes: 2 additions & 0 deletions lib/timetable/patch/entity/patch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class TimetablePatchSet extends TimetablePatchEntry {

class BuiltinTimetablePatchSet implements TimetablePatchSet {
final String key;
final bool Function(SitTimetable timetable)? recommended;

@override
String get name => "timetable.patch.builtin.$key".tr();
Expand All @@ -269,6 +270,7 @@ class BuiltinTimetablePatchSet implements TimetablePatchSet {
const BuiltinTimetablePatchSet({
required this.key,
required this.patches,
required this.recommended,
});

@override
Expand Down
184 changes: 149 additions & 35 deletions lib/timetable/patch/page/patch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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:mimir/design/animation/animated.dart';
import 'package:reorderables/reorderables.dart';
import 'package:rettulf/rettulf.dart';
import 'package:mimir/design/adaptive/foundation.dart';
Expand All @@ -13,13 +14,15 @@ import 'package:mimir/timetable/patch/widget/shared.dart';
import 'package:mimir/utils/format.dart';
import 'package:mimir/utils/save.dart';

import '../builtin.dart';
import '../entity/patch.dart';
import 'patch_prefab.dart';
import '../../entity/timetable.dart';
import '../../i18n.dart';
import '../widget/patch_set.dart';
import '../../page/preview.dart';
import 'patch_set.dart';
import 'qrcode.dart';

class TimetablePatchEditorPage extends StatefulWidget {
final SitTimetable timetable;
Expand All @@ -40,6 +43,7 @@ class _TimetablePatchEditorPageState extends State<TimetablePatchEditorPage> {
final initialEditingKey = GlobalKey(debugLabel: "Initial editing");
final controller = ScrollController();
var anyChanged = false;
TimetablePatchSet? recommended;

void markChanged() => anyChanged |= true;

Expand All @@ -63,6 +67,17 @@ class _TimetablePatchEditorPageState extends State<TimetablePatchEditorPage> {
}
});
}
evaluateRecommendation();
}

Future<void> evaluateRecommendation() async {
if (patches.isNotEmpty) return;
await Future.delayed(Durations.extralong4);
final recommendations =
BuiltinTimetablePatchSets.all.where((set) => set.recommended?.call(widget.timetable) == true).toList();
setState(() {
recommended = recommendations.firstOrNull;
});
}

@override
Expand All @@ -73,53 +88,75 @@ class _TimetablePatchEditorPageState extends State<TimetablePatchEditorPage> {

@override
Widget build(BuildContext context) {
final recommended = this.recommended;
return PromptSaveBeforeQuitScope(
changed: anyChanged,
onSave: onSave,
child: Scaffold(
resizeToAvoidBottomInset: false,
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
title: i18n.patch.title.text(),
actions: [
PlatformTextButton(
onPressed: onSave,
child: i18n.save.text(),
body: Stack(
children: [
CustomScrollView(
slivers: [
SliverAppBar.medium(
title: i18n.patch.title.text(),
actions: [
PlatformTextButton(
onPressed: onSave,
child: i18n.save.text(),
),
buildMoreActions(),
],
),
buildMoreActions(),
if (patches.isEmpty)
SliverFillRemaining(
child: LeavingBlank(
icon: Icons.dashboard_customize,
desc: i18n.patch.noPatches,
action: FilledButton(
onPressed: openPrefab,
child: i18n.patch.addPrefab.text(),
),
),
)
else
ReorderableSliverList(
controller: controller,
onReorder: (int oldIndex, int newIndex) {
setState(() {
final patch = patches.removeAt(oldIndex);
patches.insert(newIndex, patch);
});
markChanged();
},
delegate: ReorderableSliverChildBuilderDelegate(
childCount: patches.length,
(context, i) {
final patch = patches[i];
final timetable = widget.timetable.copyWith(patches: patches.sublist(0, i + 1));
return buildPatchEntry(patch, i, timetable);
},
),
),
],
),
if (patches.isEmpty)
SliverFillRemaining(
child: LeavingBlank(
icon: Icons.dashboard_customize,
desc: i18n.patch.noPatches,
action: FilledButton(
onPressed: openPrefab,
child: i18n.patch.addPrefab.text(),
),
),
)
else
ReorderableSliverList(
controller: controller,
onReorder: (int oldIndex, int newIndex) {
AnimatedShowUp(
when: recommended != null,
builder: (ctx) => TimetablePatchEntryRecommendationCard(
recommended!,
onClose: () {
setState(() {
final patch = patches.removeAt(oldIndex);
patches.insert(newIndex, patch);
this.recommended = null;
});
},
onAdded: () {
addPatch(recommended);
setState(() {
this.recommended = null;
});
markChanged();
},
delegate: ReorderableSliverChildBuilderDelegate(
childCount: patches.length,
(context, i) {
final patch = patches[i];
final timetable = widget.timetable.copyWith(patches: patches.sublist(0, i + 1));
return buildPatchEntry(patch, i, timetable);
},
),
),
).align(at: Alignment.bottomCenter),
],
),
bottomNavigationBar: BottomAppBar(
Expand Down Expand Up @@ -299,6 +336,83 @@ class _TimetablePatchEditorPageState extends State<TimetablePatchEditorPage> {
}
}

class TimetablePatchEntryRecommendationCard extends StatelessWidget {
final TimetablePatchEntry patch;
final VoidCallback? onClose;
final VoidCallback? onAdded;

const TimetablePatchEntryRecommendationCard(
this.patch, {
super.key,
this.onClose,
this.onAdded,
});

@override
Widget build(BuildContext context) {
final onClose = this.onClose;
return [
Card.outlined(
child: [
ListTile(
title: "Recommendation".text(),
trailing: onClose == null
? null
: PlatformIconButton(
icon: Icon(context.icons.close),
onPressed: onClose,
),
),
if (patch is TimetablePatch)
buildPatch(context, patch as TimetablePatch)
else if (patch is TimetablePatchSet)
buildPatchSet(context, patch as TimetablePatchSet),
FilledButton.tonalIcon(
icon: Icon(context.icons.add),
label: "Add this".text(),
onPressed: onAdded,
).padAll(8),
].column(caa: CrossAxisAlignment.end),
),
].column(mas: MainAxisSize.min).padAll(8);
}

Widget buildPatch(BuildContext context, TimetablePatch patch) {
return ListTile(
leading: PatchIcon(icon: patch.type.icon),
title: patch.type.l10n().text(),
subtitle: patch.l10n().text(),
);
}

Widget buildPatchSet(BuildContext context, TimetablePatchSet set) {
return ListTile(
title: set.name.text(),
subtitle: TimetablePatchSetPatchesPreview(set),
onTap: () async {
await context.showSheet(
(ctx) => TimetablePatchViewerPage(
patch: set,
actions: [
PlatformTextButton(
onPressed: onAdded == null
? null
: () {
context.pop();
onAdded?.call();
},
child: i18n.add.text(),
)
],
),
dismissible: false,
useRootNavigator: true,
);
},
);
}
}

class TimetablePatchEntryDroppable extends StatefulWidget {
final TimetablePatchEntry patch;
final void Function(TimetablePatch other) onMerged;
Expand Down
14 changes: 14 additions & 0 deletions lib/timetable/patch/page/patch_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,17 @@ class _TimetablePatchSetEditorPageState extends State<TimetablePatchSetEditorPag
);
}
}

class TimetablePatchSetPatchesPreview extends StatelessWidget {
final TimetablePatchSet set;

const TimetablePatchSetPatchesPreview(
this.set, {
super.key,
});

@override
Widget build(BuildContext context) {
return set.patches.map((patch) => Icon(patch.type.icon)).toList().wrap(spacing: 4);
}
}
67 changes: 43 additions & 24 deletions lib/timetable/patch/page/qrcode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,62 @@ Future<void> onTimetablePatchFromQrCode({
required TimetablePatchEntry patch,
}) async {
await context.showSheet(
(ctx) => TimetablePatchFromQrCodeSheet(patch: patch),
(ctx) => TimetablePatchFromQrCodePage(patch: patch),
dismissible: false,
useRootNavigator: true,
);
}

class TimetablePatchFromQrCodeSheet extends ConsumerStatefulWidget {
class TimetablePatchFromQrCodePage extends ConsumerWidget {
final TimetablePatchEntry patch;

const TimetablePatchFromQrCodeSheet({
const TimetablePatchFromQrCodePage({
super.key,
required this.patch,
});

@override
ConsumerState<TimetablePatchFromQrCodeSheet> createState() => _TimetablePatchFromQrCodeSheetState();
Widget build(BuildContext context, WidgetRef ref) {
final storage = TimetableInit.storage.timetable;
final timetables = ref.watch(storage.$rows);
return TimetablePatchViewerPage(
patch: patch,
actions: [
PlatformTextButton(
onPressed: timetables.isEmpty
? null
: () async {
final timetable = await context.showSheet<SitTimetable>(
(context) => TimetablePatchUseSheet(patch: patch),
);
if (timetable == null) return;
if (!context.mounted) return;
context.pop();
},
child: i18n.use.text(),
)
],
);
}
}

class _TimetablePatchFromQrCodeSheetState extends ConsumerState<TimetablePatchFromQrCodeSheet> {
class TimetablePatchViewerPage extends ConsumerStatefulWidget {
final TimetablePatchEntry patch;
final List<Widget>? actions;

const TimetablePatchViewerPage({
super.key,
required this.patch,
this.actions,
});

@override
ConsumerState<TimetablePatchViewerPage> createState() => _TimetablePatchViewerPageState();
}

class _TimetablePatchViewerPageState extends ConsumerState<TimetablePatchViewerPage> {
@override
Widget build(BuildContext context) {
final storage = TimetableInit.storage.timetable;
final timetables = ref.watch(storage.$rows);
final patch = widget.patch;
return Scaffold(
body: CustomScrollView(
Expand All @@ -53,21 +86,7 @@ class _TimetablePatchFromQrCodeSheetState extends ConsumerState<TimetablePatchFr
TextScroll(patch.name).expanded(),
].row()
: i18n.patch.title.text(),
actions: [
PlatformTextButton(
onPressed: timetables.isEmpty
? null
: () async {
final timetable = await context.showSheet<SitTimetable>(
(context) => TimetablePatchUseSheet(patch: patch),
);
if (timetable == null) return;
if (!context.mounted) return;
context.pop();
},
child: i18n.use.text(),
)
],
actions: widget.actions,
),
if (patch is TimetablePatchSet)
SliverList.builder(
Expand Down Expand Up @@ -181,13 +200,13 @@ class TimetablePatchReceiverCard extends StatelessWidget {
child: i18n.add.text(),
),
if (onPreview != null)
OutlinedButton(
FilledButton.tonal(
onPressed: onPreview,
child: i18n.preview.text(),
),
].wrap(spacing: 4),
],
),
).padV(8),
].column(caa: CrossAxisAlignment.start).padSymmetric(v: 10, h: 15).inOutlinedCard();
}
}

0 comments on commit 766fd56

Please sign in to comment.