feat(app): Home, the Library and the studyset sheets laid out as before
Home: plain cards on the surface, 16 dp apart, the goal first and the Pomodoro timer after it in a new dashboard. The streak chip sits centred in the app bar, and its sheet shows the days inside a big flame and the week as marks over the weekdays' initials, with a word on today in a box. The week names its day as "Thursday, Oct 8" and its days carry no tooltip. The heatmap scrolls its weekdays with the weeks, outlines the days to come, and names a day's activities with its date. The goal card has a ring of 5 dp, a check once met and a pencil, and its sheet saves across its width with Remove goal below. Arranging puts an eye on each widget's corner and lifts the one picked up, with a tap the hand feels; with every widget hidden, Customise brings arranging back. The stepper moves to core, where the goal's sheet takes it too. Library: the counts line includes files (PDFs); Refresh in the app bar and pulling the list down sync now; a list that can't be read offers Retry, which reads it again; the star keeps its square corners. Studyset sheet: "Title *", the secondary title explained behind a question mark, Inherit as a box beside the colour, and the description in up to three lines with a chevron. Move sheet: round marks, the studyset itself marked as current, the top level only while it is inside another, archived studysets last under their heading, the destination named in the footer, and a new studyset to move into, made only once the move is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
770de51bff
commit
aa69be47d1
35 changed files with 1831 additions and 806 deletions
|
|
@ -97,7 +97,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **Material comes from `package:material_ui`** (Cupertino from `package:cupertino_ui`), never from `package:flutter/material.dart`, which Flutter deprecates from its November 2026 release on. Packages still on the SDK's copy get the app's theme and localizations through `MaterialUiCompatibilityBridge` in `MaterialApp.builder`; remove it once they have moved.
|
- **Material comes from `package:material_ui`** (Cupertino from `package:cupertino_ui`), never from `package:flutter/material.dart`, which Flutter deprecates from its November 2026 release on. Packages still on the SDK's copy get the app's theme and localizations through `MaterialUiCompatibilityBridge` in `MaterialApp.builder`; remove it once they have moved.
|
||||||
- **Navigation is `go_router`**: the routes in `lib/src/app/app_router.dart`, their paths in `core/router/route_paths.dart`. Four tabs (Home, Library, Explore, Profile) are the branches of a `StatefulShellRoute`, each keeping its own stack of screens alive while another tab shows (`tabsContainer`, go_router's indexed stack plus `TabVisibility`, which a screen depends on to stop something once its tab is hidden); `AppShell` shows them as a navigation rail from 600 dp on and as a navigation bar below, and choosing the open tab again returns to its first screen. A tab's own screens are routes nested in its branch, reached with `context.go` (a studyset's screen in the library), and a studyset's topic opens on top of it with `context.push` (the same route, keyed by id, so several can stack); the breadcrumb's steps go up with `context.go`, so back then leads further up. A studyset's notes and learning paths (working through one, writing it, writing a step) are routes of the Library branch too, so the tabs stay beside them. What needs the whole window (studying, a card, a PDF, settings) is a top-level route over the tabs, reached with `context.push`. Explore stays empty until something is shared.
|
- **Navigation is `go_router`**: the routes in `lib/src/app/app_router.dart`, their paths in `core/router/route_paths.dart`. Four tabs (Home, Library, Explore, Profile) are the branches of a `StatefulShellRoute`, each keeping its own stack of screens alive while another tab shows (`tabsContainer`, go_router's indexed stack plus `TabVisibility`, which a screen depends on to stop something once its tab is hidden); `AppShell` shows them as a navigation rail from 600 dp on and as a navigation bar below, and choosing the open tab again returns to its first screen. A tab's own screens are routes nested in its branch, reached with `context.go` (a studyset's screen in the library), and a studyset's topic opens on top of it with `context.push` (the same route, keyed by id, so several can stack); the breadcrumb's steps go up with `context.go`, so back then leads further up. A studyset's notes and learning paths (working through one, writing it, writing a step) are routes of the Library branch too, so the tabs stay beside them. What needs the whole window (studying, a card, a PDF, settings) is a top-level route over the tabs, reached with `context.push`. Explore stays empty until something is shared.
|
||||||
- **Routes carry ids, never objects.** A screen finds its item in the repository's live list with `FoundById` (`core/widget/`): a spinner until the list arrives, then the item, or a screen saying it no longer exists (deleted on another device, or an old link). A screen that deletes its item closes, and keeps showing the item while it goes. Why: every location can be built from its path alone, and no screen goes on showing something that has gone.
|
- **Routes carry ids, never objects.** A screen finds its item in the repository's live list with `FoundById` (`core/widget/`): a spinner until the list arrives, then the item, or a screen saying it no longer exists (deleted on another device, or an old link). A screen that deletes its item closes, and keeps showing the item while it goes. Why: every location can be built from its path alone, and no screen goes on showing something that has gone.
|
||||||
- **The look is one theme** (`core/theme/`): `appTheme(brightness)` with hand-written light and dark colour schemes around the brand's cobalt and copper; `Theme.of(context).extras` (`ThemeExtras`) for the colours Material has no role for (states, the four FSRS ratings, a starred star); `PaletteColor` for the 20 colours a person can give content. `theme_test.dart` holds text to WCAG AA contrast (4.5:1) on each palette colour and on the dark theme's primary colour and surface. Layouts follow Material 3's window-size classes (`context.windowSize`, `core/layout/`), and running text stays within `readableContentMaxWidth`. Icons are Lucide (`lucide_icons_flutter`), except a starred star and the marks of a choice not yet checked (filled box and circle), which need the fill Lucide lacks; the Material icons left in older screens go when those are restyled. Shared widgets are in `core/widget/` (`SectionCard`, `AppLoadingIndicator`, `confirmDestructiveAction`, `FoundById`, `TitleWithKicker`, `ContentCountsLine`, `showUndoSnackBar`, `ScrollHidingFab`, `PaletteColorPicker`, and the sheets below).
|
- **The look is one theme** (`core/theme/`): `appTheme(brightness)` with hand-written light and dark colour schemes around the brand's cobalt and copper; `Theme.of(context).extras` (`ThemeExtras`) for the colours Material has no role for (states, the four FSRS ratings, a starred star); `PaletteColor` for the 20 colours a person can give content. `theme_test.dart` holds text to WCAG AA contrast (4.5:1) on each palette colour and on the dark theme's primary colour and surface. Layouts follow Material 3's window-size classes (`context.windowSize`, `core/layout/`), and running text stays within `readableContentMaxWidth`. Icons are Lucide (`lucide_icons_flutter`), except a starred star and the marks of a choice not yet checked (filled box and circle), which need the fill Lucide lacks; the Material icons left in older screens go when those are restyled. Shared widgets are in `core/widget/` (`SectionCard`, `AppLoadingIndicator`, `confirmDestructiveAction`, `FoundById`, `TitleWithKicker`, `ContentCountsLine`, `showUndoSnackBar`, `ScrollHidingFab`, `PaletteColorPicker`, `CountStepper`, `RequiredFieldLabel` and `FieldInfoButton`, and the sheets below).
|
||||||
- **Forms and pickers open as sheets**: `showAdaptiveSheet` with a `SheetScaffold` (title and close button on top, the body scrolling with a scrollbar, always shown on the desktop, and Material 3's lines under the title and above the footer while some of the body lies beyond them, the action pinned below) shows a bottom sheet below expanded windows and a dialog from there on. A sheet that asks before discarding changes (a `PopScope`) passes `swipeable: false`: a bottom sheet swiped away closes without asking its `PopScope` (Flutter issue 152120, open with v3.47). A sheet offering an Undo passes `hasSnackBars: true`, so its snack bars show on it rather than behind its barrier. A picker that changes something outside it reports every change as it happens (`onChanged`) rather than as its result, since a sheet swiped away has none.
|
- **Forms and pickers open as sheets**: `showAdaptiveSheet` with a `SheetScaffold` (title and close button on top, the body scrolling with a scrollbar, always shown on the desktop, and Material 3's lines under the title and above the footer while some of the body lies beyond them, the action pinned below) shows a bottom sheet below expanded windows and a dialog from there on. A sheet that asks before discarding changes (a `PopScope`) passes `swipeable: false`: a bottom sheet swiped away closes without asking its `PopScope` (Flutter issue 152120, open with v3.47). A sheet offering an Undo passes `hasSnackBars: true`, so its snack bars show on it rather than behind its barrier. A picker that changes something outside it reports every change as it happens (`onChanged`) rather than as its result, since a sheet swiped away has none.
|
||||||
- **Text comes from `AppLocalizations`** (Flutter's gen-l10n, `lib/l10n/`): no string a person reads is written into a widget. The app speaks British English, American English and German (informal "du"; the terms of `app_de.arb`, such as Lernset and Karteikarte, stay consistent). `app_en.arb` is the template and holds British English, also the fallback for any language the app doesn't speak; `app_en_US.arb` overrides only what American English spells differently, and `app_en_GB.arb` is empty, there so that Material and dates format the British way. A new message goes into `app_en.arb` and `app_de.arb` together (`test/l10n/arb_files_test.dart` fails otherwise, and checks the placeholders), and into `app_en_US.arb` if its spelling differs. `resolveAppLocale` (`core/l10n/`) picks the language, never by country alone, and both `MaterialApp`s take `appLocalizationsDelegates`, not gen-l10n's list: that brings flutter_localizations' Material texts, which serve only the SDK's Material, so material_ui's widgets would stay English. Errors found in data go up as values the widgets put into words (`PathImportProblem`, `PathImportTexts`), never as English sentences. The editor's own placeholders are replaced by ours (`buildersWithUnknown`), since the editor's texts follow a process-wide locale of their own. The generated `lib/l10n/app_localizations*.dart` are committed and never edited, analysed or formatted by hand. `flutter pub get` writes them anew, so CI fails when they don't match the `.arb` files.
|
- **Text comes from `AppLocalizations`** (Flutter's gen-l10n, `lib/l10n/`): no string a person reads is written into a widget. The app speaks British English, American English and German (informal "du"; the terms of `app_de.arb`, such as Lernset and Karteikarte, stay consistent). `app_en.arb` is the template and holds British English, also the fallback for any language the app doesn't speak; `app_en_US.arb` overrides only what American English spells differently, and `app_en_GB.arb` is empty, there so that Material and dates format the British way. A new message goes into `app_en.arb` and `app_de.arb` together (`test/l10n/arb_files_test.dart` fails otherwise, and checks the placeholders), and into `app_en_US.arb` if its spelling differs. `resolveAppLocale` (`core/l10n/`) picks the language, never by country alone, and both `MaterialApp`s take `appLocalizationsDelegates`, not gen-l10n's list: that brings flutter_localizations' Material texts, which serve only the SDK's Material, so material_ui's widgets would stay English. Errors found in data go up as values the widgets put into words (`PathImportProblem`, `PathImportTexts`), never as English sentences. The editor's own placeholders are replaced by ours (`buildersWithUnknown`), since the editor's texts follow a process-wide locale of their own. The generated `lib/l10n/app_localizations*.dart` are committed and never edited, analysed or formatted by hand. `flutter pub get` writes them anew, so CI fails when they don't match the `.arb` files.
|
||||||
|
|
||||||
|
|
@ -151,7 +151,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
|
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
|
||||||
- **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device.
|
- **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device.
|
||||||
- **Older apps** can't be made to update once published. Before the first sync of each launch the app asks whether the server still works with its build (`minimumAppBuild` in `instance.info()`, from `lib/src/instance/app_compatibility.dart` in the server; the build is the number after `+` in the app's version), and the sync engine refuses an app whose synced tables differ from the server's. Both end in `AppServerMismatchException` (`core/sync/`), and the app says so: a banner on Home and the Sync card in Settings. Raise `minimumAppBuild` in the commit that breaks older apps (an endpoint they call changes or goes), to the build of the first app that works again; a change to the synced tables needs no raise. Every release raises the build number.
|
- **Older apps** can't be made to update once published. Before the first sync of each launch the app asks whether the server still works with its build (`minimumAppBuild` in `instance.info()`, from `lib/src/instance/app_compatibility.dart` in the server; the build is the number after `+` in the app's version), and the sync engine refuses an app whose synced tables differ from the server's. Both end in `AppServerMismatchException` (`core/sync/`), and the app says so: a banner on Home and the Sync card in Settings. Raise `minimumAppBuild` in the commit that breaks older apps (an endpoint they call changes or goes), to the build of the first app that works again; a change to the synced tables needs no raise. Every release raises the build number.
|
||||||
- **Where sync stands** is `SyncService.status` (syncing, the last sync that went through, the last failure; since the app started), which `SyncStatusBloc` (`core/sync/`, provided by `PersonApp`) hands to the trouble dot on the Settings cogs (on Home and Profile; no tab carries it), the Sync card in Settings and the banner on Home. Only the last outcome counts: one sync that goes through clears a failure.
|
- **Where sync stands** is `SyncService.status` (syncing, the last sync that went through, the last failure; since the app started), which `SyncStatusBloc` (`core/sync/`, provided by `PersonApp`) hands to the trouble dot on the Settings cogs (on Home and Profile; no tab carries it), the Sync card in Settings and the banner on Home. Only the last outcome counts: one sync that goes through clears a failure. The Library's Refresh and pulling it down sync now (`syncAndWait`, which a pull's spinner waits on).
|
||||||
- **Serverpod's version moves together with `serverpod_offline_sync`**, which pins Serverpod exactly: the root `pubspec.yaml` overrides the four core packages to the project's version. Upgrade Serverpod only together with those overrides and the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`), after reading the sync package's changelog.
|
- **Serverpod's version moves together with `serverpod_offline_sync`**, which pins Serverpod exactly: the root `pubspec.yaml` overrides the four core packages to the project's version. Upgrade Serverpod only together with those overrides and the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`), after reading the sync package's changelog.
|
||||||
- **The source-code link**: both licences require offering the source of what an instance runs to the people who use it. `instance.info().sourceCodeUrl` names it (the operator's `SOURCE_URL`, else this project's repository) and the app's About dialog shows it next to the licences and every package's notice. Keep it working when changing the dialog or the endpoint.
|
- **The source-code link**: both licences require offering the source of what an instance runs to the people who use it. `instance.info().sourceCodeUrl` names it (the operator's `SOURCE_URL`, else this project's repository) and the app's About dialog shows it next to the licences and every package's notice. Keep it working when changing the dialog or the endpoint.
|
||||||
- **Deleting a person** goes through `deletePerson` (`lib/src/auth/account.dart`), in one transaction and in ADR 0001 rule 6's order: their sessions end first (a device with a valid session would re-upload everything), then their space goes with all its content through the foreign keys, then the person with their identities. `AccountEndpoint.delete` deletes the caller; the app also calls it when an anonymous account is given up (signed out of, or switched away from to GitHub), since nobody could ever sign into it again.
|
- **Deleting a person** goes through `deletePerson` (`lib/src/auth/account.dart`), in one transaction and in ADR 0001 rule 6's order: their sessions end first (a device with a valid session would re-upload everything), then their space goes with all its content through the foreign keys, then the person with their identities. `AccountEndpoint.delete` deletes the caller; the app also calls it when an anonymous account is given up (signed out of, or switched away from to GitHub), since nobody could ever sign into it again.
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
"studysetSheetSave": "Speichern",
|
"studysetSheetSave": "Speichern",
|
||||||
"studysetSheetTitleLabel": "Titel",
|
"studysetSheetTitleLabel": "Titel",
|
||||||
"studysetSheetSecondaryLabel": "Zweittitel",
|
"studysetSheetSecondaryLabel": "Zweittitel",
|
||||||
"studysetSheetSecondaryHelper": "Wird als kleines Label über dem Titel angezeigt.",
|
"studysetSheetSecondaryHelper": "Wird als kleines Label über dem Titel auf Karten und im Kopf des Lernsets angezeigt. Pfade nutzen es, um Stufen zu kennzeichnen (Challenge, Meilenstein, Lektion).",
|
||||||
"studysetSheetColor": "Farbe",
|
"studysetSheetColor": "Farbe",
|
||||||
"studysetSheetDiscardTitle": "Änderungen verwerfen?",
|
"studysetSheetDiscardTitle": "Änderungen verwerfen?",
|
||||||
"studysetSheetDiscardMessage": "Du hast ungespeicherte Änderungen an diesem Lernset. Verwerfen und das Formular schließen?",
|
"studysetSheetDiscardMessage": "Du hast ungespeicherte Änderungen an diesem Lernset. Verwerfen und das Formular schließen?",
|
||||||
|
|
@ -44,6 +44,9 @@
|
||||||
|
|
||||||
"studysetCountFlashcards": "{count, plural, =1{1 Karteikarte} other{{count} Karteikarten}}",
|
"studysetCountFlashcards": "{count, plural, =1{1 Karteikarte} other{{count} Karteikarten}}",
|
||||||
"studysetCountNotes": "{count, plural, =1{1 Notiz} other{{count} Notizen}}",
|
"studysetCountNotes": "{count, plural, =1{1 Notiz} other{{count} Notizen}}",
|
||||||
|
"studysetCountFiles": "{count, plural, =1{1 Datei} other{{count} Dateien}}",
|
||||||
|
"libraryRefresh": "Aktualisieren",
|
||||||
|
"libraryRetry": "Erneut versuchen",
|
||||||
"studysetNoContent": "Noch kein Inhalt",
|
"studysetNoContent": "Noch kein Inhalt",
|
||||||
"studysetActionReview": "Wiederholen",
|
"studysetActionReview": "Wiederholen",
|
||||||
"studysetActionStudy": "Lernen",
|
"studysetActionStudy": "Lernen",
|
||||||
|
|
@ -233,18 +236,25 @@
|
||||||
"studysetDragToReorder": "Zum Umsortieren ziehen",
|
"studysetDragToReorder": "Zum Umsortieren ziehen",
|
||||||
"studysetMoveTitle": "Lernset verschieben nach",
|
"studysetMoveTitle": "Lernset verschieben nach",
|
||||||
"studysetMoveTopLevel": "Oberste Ebene",
|
"studysetMoveTopLevel": "Oberste Ebene",
|
||||||
"studysetMoveCurrent": "Aktueller Ort",
|
"studysetMoveCurrent": "(aktuell)",
|
||||||
|
"studysetMoveTopLevelRow": "Oberste Ebene (in keinem Lernset)",
|
||||||
|
"studysetMoveArchived": "Archiviert",
|
||||||
|
"studysetMoveNew": "Neues Lernset",
|
||||||
|
"studysetMoveSelectDestination": "Wähle ein Ziel",
|
||||||
|
"studysetMoveDestination": "Ziel: {name}",
|
||||||
"studysetMoveConfirm": "Verschieben",
|
"studysetMoveConfirm": "Verschieben",
|
||||||
"studysetMoveShowInside": "Inhalt anzeigen",
|
"studysetMoveShowInside": "Unter-Lernsets anzeigen",
|
||||||
"studysetMoveHideInside": "Inhalt ausblenden",
|
"studysetMoveHideInside": "Unter-Lernsets ausblenden",
|
||||||
"studysetMoved": "Nach „{destination}“ verschoben.",
|
"studysetMoved": "„{title}“ nach {destination} verschoben.",
|
||||||
"studysetReadMore": "Mehr anzeigen",
|
"studysetReadMore": "Mehr anzeigen",
|
||||||
"studysetDescriptionTitle": "Beschreibung",
|
"studysetDescriptionTitle": "Beschreibung",
|
||||||
"studysetDescriptionEditTitle": "Beschreibung bearbeiten",
|
"studysetDescriptionEditTitle": "Beschreibung bearbeiten",
|
||||||
"richTextSave": "Speichern",
|
"richTextSave": "Speichern",
|
||||||
"studysetSheetDescription": "Beschreibung",
|
"studysetSheetDescription": "Beschreibung",
|
||||||
"studysetSheetDescriptionAdd": "Beschreibung hinzufügen",
|
"studysetSheetDescriptionAdd": "Tippen, um eine Beschreibung hinzuzufügen",
|
||||||
"studysetSheetInheritColor": "Gleiche Farbe wie das Lernset, in dem es liegt",
|
"studysetSheetDescriptionAdded": "Beschreibung hinzugefügt — zum Bearbeiten tippen",
|
||||||
|
"studysetSheetInheritColor": "Erben",
|
||||||
|
"fieldRequired": "Pflichtfeld",
|
||||||
|
|
||||||
"exploreTitle": "Entdecken",
|
"exploreTitle": "Entdecken",
|
||||||
"exploreEmpty": "Lernsets, die andere teilen, erscheinen hier.",
|
"exploreEmpty": "Lernsets, die andere teilen, erscheinen hier.",
|
||||||
|
|
@ -602,8 +612,9 @@
|
||||||
"homeShowWidget": "{label} anzeigen",
|
"homeShowWidget": "{label} anzeigen",
|
||||||
"homeHideWidget": "{label} ausblenden",
|
"homeHideWidget": "{label} ausblenden",
|
||||||
"homeAllHidden": "Alle Widgets sind ausgeblendet",
|
"homeAllHidden": "Alle Widgets sind ausgeblendet",
|
||||||
"homeAllHiddenHint": "Halte hier lange gedrückt, um welche zurückzuholen.",
|
"homeAllHiddenBody": "Blende einige wieder ein, um dein Dashboard zu füllen.",
|
||||||
"homeWidgetGoal": "Lernziel",
|
"homeCustomise": "Anpassen",
|
||||||
|
"homeWidgetGoal": "Tagesziel",
|
||||||
"homeWidgetWeek": "Wochenübersicht",
|
"homeWidgetWeek": "Wochenübersicht",
|
||||||
"homeWidgetActivity": "Aktivitäts-Heatmap",
|
"homeWidgetActivity": "Aktivitäts-Heatmap",
|
||||||
"homeActivityTitle": "Deine Aktivität",
|
"homeActivityTitle": "Deine Aktivität",
|
||||||
|
|
@ -635,11 +646,10 @@
|
||||||
"weekPreviousDay": "Vorheriger Tag",
|
"weekPreviousDay": "Vorheriger Tag",
|
||||||
"weekNextDay": "Nächster Tag",
|
"weekNextDay": "Nächster Tag",
|
||||||
"weekRestDay": "Ruhetag",
|
"weekRestDay": "Ruhetag",
|
||||||
"weekDueCount": "{count, plural, =1{1 Karte fällig} other{{count} Karten fällig}}",
|
|
||||||
"heatmapLess": "Weniger",
|
"heatmapLess": "Weniger",
|
||||||
"heatmapMore": "Mehr",
|
"heatmapMore": "Mehr",
|
||||||
"heatmapTotal": "{count, plural, =1{1 Aktivität im letzten Jahr} other{{count} Aktivitäten im letzten Jahr}}",
|
"heatmapTotal": "{count, plural, =1{1 Aktivität im letzten Jahr} other{{count} Aktivitäten im letzten Jahr}}",
|
||||||
"heatmapDay": "{count, plural, =0{Nichts} =1{1 Aktivität} other{{count} Aktivitäten}} · {date}",
|
"heatmapDay": "{count, plural, =1{1 Aktivität} other{{count} Aktivitäten}} · {date}",
|
||||||
|
|
||||||
"commonCancel": "Abbrechen",
|
"commonCancel": "Abbrechen",
|
||||||
"commonSave": "Speichern",
|
"commonSave": "Speichern",
|
||||||
|
|
@ -882,7 +892,7 @@
|
||||||
"readerCaptureFailed": "Die Seite konnte nicht dargestellt werden. Versuche es erneut, sobald sie geladen ist.",
|
"readerCaptureFailed": "Die Seite konnte nicht dargestellt werden. Versuche es erneut, sobald sie geladen ist.",
|
||||||
"readerScreenshotSaved": "Bildschirmfoto gespeichert",
|
"readerScreenshotSaved": "Bildschirmfoto gespeichert",
|
||||||
"imageOpenPdfPage": "PDF auf dieser Seite öffnen",
|
"imageOpenPdfPage": "PDF auf dieser Seite öffnen",
|
||||||
"homeWidgetDeadlines": "Lesefristen",
|
"homeWidgetDeadlines": "PDF-Lesefristen",
|
||||||
"deadlinesTitle": "Lesefristen",
|
"deadlinesTitle": "Lesefristen",
|
||||||
"deadlinesCurrent": "Aktuell",
|
"deadlinesCurrent": "Aktuell",
|
||||||
"deadlinesUpcoming": "Demnächst",
|
"deadlinesUpcoming": "Demnächst",
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"studysetSheetSave": "Save",
|
"studysetSheetSave": "Save",
|
||||||
"studysetSheetTitleLabel": "Title",
|
"studysetSheetTitleLabel": "Title",
|
||||||
"studysetSheetSecondaryLabel": "Secondary title",
|
"studysetSheetSecondaryLabel": "Secondary title",
|
||||||
"studysetSheetSecondaryHelper": "Shown as a small label above the title.",
|
"studysetSheetSecondaryHelper": "Shown as a small label above the title on cards and the studyset's header. Paths use it to label tiers (Challenge, Milestone, Lesson).",
|
||||||
"studysetSheetColor": "Colour",
|
"studysetSheetColor": "Colour",
|
||||||
"studysetSheetDiscardTitle": "Discard changes?",
|
"studysetSheetDiscardTitle": "Discard changes?",
|
||||||
"studysetSheetDiscardMessage": "You have unsaved changes to this studyset. Discard them and close the form?",
|
"studysetSheetDiscardMessage": "You have unsaved changes to this studyset. Discard them and close the form?",
|
||||||
|
|
@ -49,6 +49,10 @@
|
||||||
"@studysetCountFlashcards": {"placeholders": {"count": {"type": "int"}}},
|
"@studysetCountFlashcards": {"placeholders": {"count": {"type": "int"}}},
|
||||||
"studysetCountNotes": "{count, plural, =1{1 note} other{{count} notes}}",
|
"studysetCountNotes": "{count, plural, =1{1 note} other{{count} notes}}",
|
||||||
"@studysetCountNotes": {"placeholders": {"count": {"type": "int"}}},
|
"@studysetCountNotes": {"placeholders": {"count": {"type": "int"}}},
|
||||||
|
"studysetCountFiles": "{count, plural, =1{1 file} other{{count} files}}",
|
||||||
|
"@studysetCountFiles": {"placeholders": {"count": {"type": "int"}}},
|
||||||
|
"libraryRefresh": "Refresh",
|
||||||
|
"libraryRetry": "Retry",
|
||||||
"studysetNoContent": "No content yet",
|
"studysetNoContent": "No content yet",
|
||||||
"studysetActionReview": "Review",
|
"studysetActionReview": "Review",
|
||||||
"studysetActionStudy": "Study",
|
"studysetActionStudy": "Study",
|
||||||
|
|
@ -257,19 +261,27 @@
|
||||||
"studysetDragToReorder": "Drag to reorder",
|
"studysetDragToReorder": "Drag to reorder",
|
||||||
"studysetMoveTitle": "Move studyset to",
|
"studysetMoveTitle": "Move studyset to",
|
||||||
"studysetMoveTopLevel": "Top level",
|
"studysetMoveTopLevel": "Top level",
|
||||||
"studysetMoveCurrent": "Where it is now",
|
"studysetMoveCurrent": "(current)",
|
||||||
|
"studysetMoveTopLevelRow": "Top level (no parent)",
|
||||||
|
"studysetMoveArchived": "Archived",
|
||||||
|
"studysetMoveNew": "New studyset",
|
||||||
|
"studysetMoveSelectDestination": "Select a destination",
|
||||||
|
"studysetMoveDestination": "Destination: {name}",
|
||||||
|
"@studysetMoveDestination": {"placeholders": {"name": {"type": "String"}}},
|
||||||
"studysetMoveConfirm": "Move",
|
"studysetMoveConfirm": "Move",
|
||||||
"studysetMoveShowInside": "Show what is inside",
|
"studysetMoveShowInside": "Show sub-studysets",
|
||||||
"studysetMoveHideInside": "Hide what is inside",
|
"studysetMoveHideInside": "Hide sub-studysets",
|
||||||
"studysetMoved": "Moved to {destination}.",
|
"studysetMoved": "Moved \"{title}\" to {destination}.",
|
||||||
"@studysetMoved": {"placeholders": {"destination": {"type": "String"}}},
|
"@studysetMoved": {"placeholders": {"title": {"type": "String"}, "destination": {"type": "String"}}},
|
||||||
"studysetReadMore": "Read more",
|
"studysetReadMore": "Read more",
|
||||||
"studysetDescriptionTitle": "Description",
|
"studysetDescriptionTitle": "Description",
|
||||||
"studysetDescriptionEditTitle": "Edit description",
|
"studysetDescriptionEditTitle": "Edit description",
|
||||||
"richTextSave": "Save",
|
"richTextSave": "Save",
|
||||||
"studysetSheetDescription": "Description",
|
"studysetSheetDescription": "Description",
|
||||||
"studysetSheetDescriptionAdd": "Add a description",
|
"studysetSheetDescriptionAdd": "Tap to add a description",
|
||||||
"studysetSheetInheritColor": "Same colour as the studyset it is in",
|
"studysetSheetDescriptionAdded": "Description added — tap to edit",
|
||||||
|
"studysetSheetInheritColor": "Inherit",
|
||||||
|
"fieldRequired": "required",
|
||||||
|
|
||||||
"exploreTitle": "Explore",
|
"exploreTitle": "Explore",
|
||||||
"exploreEmpty": "Studysets that others share will appear here.",
|
"exploreEmpty": "Studysets that others share will appear here.",
|
||||||
|
|
@ -677,8 +689,9 @@
|
||||||
"homeHideWidget": "Hide {label}",
|
"homeHideWidget": "Hide {label}",
|
||||||
"@homeHideWidget": {"placeholders": {"label": {"type": "String"}}},
|
"@homeHideWidget": {"placeholders": {"label": {"type": "String"}}},
|
||||||
"homeAllHidden": "All widgets are hidden",
|
"homeAllHidden": "All widgets are hidden",
|
||||||
"homeAllHiddenHint": "Long-press here to bring some back.",
|
"homeAllHiddenBody": "Bring some back to fill your dashboard.",
|
||||||
"homeWidgetGoal": "Study goal",
|
"homeCustomise": "Customise",
|
||||||
|
"homeWidgetGoal": "Daily goal",
|
||||||
"homeWidgetWeek": "Week overview",
|
"homeWidgetWeek": "Week overview",
|
||||||
"homeWidgetActivity": "Activity heatmap",
|
"homeWidgetActivity": "Activity heatmap",
|
||||||
"homeActivityTitle": "Your activity",
|
"homeActivityTitle": "Your activity",
|
||||||
|
|
@ -713,13 +726,11 @@
|
||||||
"weekPreviousDay": "Previous day",
|
"weekPreviousDay": "Previous day",
|
||||||
"weekNextDay": "Next day",
|
"weekNextDay": "Next day",
|
||||||
"weekRestDay": "Rest day",
|
"weekRestDay": "Rest day",
|
||||||
"weekDueCount": "{count, plural, =1{1 card due} other{{count} cards due}}",
|
|
||||||
"@weekDueCount": {"placeholders": {"count": {"type": "int"}}},
|
|
||||||
"heatmapLess": "Less",
|
"heatmapLess": "Less",
|
||||||
"heatmapMore": "More",
|
"heatmapMore": "More",
|
||||||
"heatmapTotal": "{count, plural, =1{1 activity in the last year} other{{count} activities in the last year}}",
|
"heatmapTotal": "{count, plural, =1{1 activity in the last year} other{{count} activities in the last year}}",
|
||||||
"@heatmapTotal": {"placeholders": {"count": {"type": "int"}}},
|
"@heatmapTotal": {"placeholders": {"count": {"type": "int"}}},
|
||||||
"heatmapDay": "{count, plural, =0{Nothing} =1{1 activity} other{{count} activities}} · {date}",
|
"heatmapDay": "{count, plural, =1{1 activity} other{{count} activities}} · {date}",
|
||||||
"@heatmapDay": {"placeholders": {"count": {"type": "int"}, "date": {"type": "String"}}},
|
"@heatmapDay": {"placeholders": {"count": {"type": "int"}, "date": {"type": "String"}}},
|
||||||
|
|
||||||
"commonCancel": "Cancel",
|
"commonCancel": "Cancel",
|
||||||
|
|
@ -1006,7 +1017,7 @@
|
||||||
"readerCaptureFailed": "The page couldn't be drawn. Try again once it has loaded.",
|
"readerCaptureFailed": "The page couldn't be drawn. Try again once it has loaded.",
|
||||||
"readerScreenshotSaved": "Screenshot saved",
|
"readerScreenshotSaved": "Screenshot saved",
|
||||||
"imageOpenPdfPage": "Open the PDF at this page",
|
"imageOpenPdfPage": "Open the PDF at this page",
|
||||||
"homeWidgetDeadlines": "Reading deadlines",
|
"homeWidgetDeadlines": "PDF reading deadlines",
|
||||||
"deadlinesTitle": "Reading deadlines",
|
"deadlinesTitle": "Reading deadlines",
|
||||||
"deadlinesCurrent": "Current",
|
"deadlinesCurrent": "Current",
|
||||||
"deadlinesUpcoming": "Upcoming",
|
"deadlinesUpcoming": "Upcoming",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
"studysetSheetColor": "Color",
|
"studysetSheetColor": "Color",
|
||||||
"colorPickerLabel": "Color: {color}",
|
"colorPickerLabel": "Color: {color}",
|
||||||
"studysetSheetInheritColor": "Same color as the studyset it is in",
|
|
||||||
"tagSheetColor": "Color",
|
"tagSheetColor": "Color",
|
||||||
"formatAlignCenter": "Center"
|
"formatAlignCenter": "Center",
|
||||||
|
"homeCustomise": "Customize"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @studysetSheetSecondaryHelper.
|
/// No description provided for @studysetSheetSecondaryHelper.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Shown as a small label above the title.'**
|
/// **'Shown as a small label above the title on cards and the studyset\'s header. Paths use it to label tiers (Challenge, Milestone, Lesson).'**
|
||||||
String get studysetSheetSecondaryHelper;
|
String get studysetSheetSecondaryHelper;
|
||||||
|
|
||||||
/// No description provided for @studysetSheetColor.
|
/// No description provided for @studysetSheetColor.
|
||||||
|
|
@ -322,6 +322,24 @@ abstract class AppLocalizations {
|
||||||
/// **'{count, plural, =1{1 note} other{{count} notes}}'**
|
/// **'{count, plural, =1{1 note} other{{count} notes}}'**
|
||||||
String studysetCountNotes(int count);
|
String studysetCountNotes(int count);
|
||||||
|
|
||||||
|
/// No description provided for @studysetCountFiles.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count, plural, =1{1 file} other{{count} files}}'**
|
||||||
|
String studysetCountFiles(int count);
|
||||||
|
|
||||||
|
/// No description provided for @libraryRefresh.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh'**
|
||||||
|
String get libraryRefresh;
|
||||||
|
|
||||||
|
/// No description provided for @libraryRetry.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Retry'**
|
||||||
|
String get libraryRetry;
|
||||||
|
|
||||||
/// No description provided for @studysetNoContent.
|
/// No description provided for @studysetNoContent.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1459,9 +1477,39 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @studysetMoveCurrent.
|
/// No description provided for @studysetMoveCurrent.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Where it is now'**
|
/// **'(current)'**
|
||||||
String get studysetMoveCurrent;
|
String get studysetMoveCurrent;
|
||||||
|
|
||||||
|
/// No description provided for @studysetMoveTopLevelRow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Top level (no parent)'**
|
||||||
|
String get studysetMoveTopLevelRow;
|
||||||
|
|
||||||
|
/// No description provided for @studysetMoveArchived.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Archived'**
|
||||||
|
String get studysetMoveArchived;
|
||||||
|
|
||||||
|
/// No description provided for @studysetMoveNew.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New studyset'**
|
||||||
|
String get studysetMoveNew;
|
||||||
|
|
||||||
|
/// No description provided for @studysetMoveSelectDestination.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a destination'**
|
||||||
|
String get studysetMoveSelectDestination;
|
||||||
|
|
||||||
|
/// No description provided for @studysetMoveDestination.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Destination: {name}'**
|
||||||
|
String studysetMoveDestination(String name);
|
||||||
|
|
||||||
/// No description provided for @studysetMoveConfirm.
|
/// No description provided for @studysetMoveConfirm.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1471,20 +1519,20 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @studysetMoveShowInside.
|
/// No description provided for @studysetMoveShowInside.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Show what is inside'**
|
/// **'Show sub-studysets'**
|
||||||
String get studysetMoveShowInside;
|
String get studysetMoveShowInside;
|
||||||
|
|
||||||
/// No description provided for @studysetMoveHideInside.
|
/// No description provided for @studysetMoveHideInside.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Hide what is inside'**
|
/// **'Hide sub-studysets'**
|
||||||
String get studysetMoveHideInside;
|
String get studysetMoveHideInside;
|
||||||
|
|
||||||
/// No description provided for @studysetMoved.
|
/// No description provided for @studysetMoved.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Moved to {destination}.'**
|
/// **'Moved \"{title}\" to {destination}.'**
|
||||||
String studysetMoved(String destination);
|
String studysetMoved(String title, String destination);
|
||||||
|
|
||||||
/// No description provided for @studysetReadMore.
|
/// No description provided for @studysetReadMore.
|
||||||
///
|
///
|
||||||
|
|
@ -1519,15 +1567,27 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @studysetSheetDescriptionAdd.
|
/// No description provided for @studysetSheetDescriptionAdd.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Add a description'**
|
/// **'Tap to add a description'**
|
||||||
String get studysetSheetDescriptionAdd;
|
String get studysetSheetDescriptionAdd;
|
||||||
|
|
||||||
|
/// No description provided for @studysetSheetDescriptionAdded.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Description added — tap to edit'**
|
||||||
|
String get studysetSheetDescriptionAdded;
|
||||||
|
|
||||||
/// No description provided for @studysetSheetInheritColor.
|
/// No description provided for @studysetSheetInheritColor.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Same colour as the studyset it is in'**
|
/// **'Inherit'**
|
||||||
String get studysetSheetInheritColor;
|
String get studysetSheetInheritColor;
|
||||||
|
|
||||||
|
/// No description provided for @fieldRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'required'**
|
||||||
|
String get fieldRequired;
|
||||||
|
|
||||||
/// No description provided for @exploreTitle.
|
/// No description provided for @exploreTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3610,16 +3670,22 @@ abstract class AppLocalizations {
|
||||||
/// **'All widgets are hidden'**
|
/// **'All widgets are hidden'**
|
||||||
String get homeAllHidden;
|
String get homeAllHidden;
|
||||||
|
|
||||||
/// No description provided for @homeAllHiddenHint.
|
/// No description provided for @homeAllHiddenBody.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Long-press here to bring some back.'**
|
/// **'Bring some back to fill your dashboard.'**
|
||||||
String get homeAllHiddenHint;
|
String get homeAllHiddenBody;
|
||||||
|
|
||||||
|
/// No description provided for @homeCustomise.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Customise'**
|
||||||
|
String get homeCustomise;
|
||||||
|
|
||||||
/// No description provided for @homeWidgetGoal.
|
/// No description provided for @homeWidgetGoal.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Study goal'**
|
/// **'Daily goal'**
|
||||||
String get homeWidgetGoal;
|
String get homeWidgetGoal;
|
||||||
|
|
||||||
/// No description provided for @homeWidgetWeek.
|
/// No description provided for @homeWidgetWeek.
|
||||||
|
|
@ -3808,12 +3874,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Rest day'**
|
/// **'Rest day'**
|
||||||
String get weekRestDay;
|
String get weekRestDay;
|
||||||
|
|
||||||
/// No description provided for @weekDueCount.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'{count, plural, =1{1 card due} other{{count} cards due}}'**
|
|
||||||
String weekDueCount(int count);
|
|
||||||
|
|
||||||
/// No description provided for @heatmapLess.
|
/// No description provided for @heatmapLess.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3835,7 +3895,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @heatmapDay.
|
/// No description provided for @heatmapDay.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'{count, plural, =0{Nothing} =1{1 activity} other{{count} activities}} · {date}'**
|
/// **'{count, plural, =1{1 activity} other{{count} activities}} · {date}'**
|
||||||
String heatmapDay(int count, String date);
|
String heatmapDay(int count, String date);
|
||||||
|
|
||||||
/// No description provided for @commonCancel.
|
/// No description provided for @commonCancel.
|
||||||
|
|
@ -5287,7 +5347,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @homeWidgetDeadlines.
|
/// No description provided for @homeWidgetDeadlines.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Reading deadlines'**
|
/// **'PDF reading deadlines'**
|
||||||
String get homeWidgetDeadlines;
|
String get homeWidgetDeadlines;
|
||||||
|
|
||||||
/// No description provided for @deadlinesTitle.
|
/// No description provided for @deadlinesTitle.
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetSecondaryHelper =>
|
String get studysetSheetSecondaryHelper =>
|
||||||
'Wird als kleines Label über dem Titel angezeigt.';
|
'Wird als kleines Label über dem Titel auf Karten und im Kopf des Lernsets angezeigt. Pfade nutzen es, um Stufen zu kennzeichnen (Challenge, Meilenstein, Lektion).';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetColor => 'Farbe';
|
String get studysetSheetColor => 'Farbe';
|
||||||
|
|
@ -171,6 +171,23 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String studysetCountFiles(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count Dateien',
|
||||||
|
one: '1 Datei',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get libraryRefresh => 'Aktualisieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get libraryRetry => 'Erneut versuchen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetNoContent => 'Noch kein Inhalt';
|
String get studysetNoContent => 'Noch kein Inhalt';
|
||||||
|
|
||||||
|
|
@ -845,20 +862,37 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get studysetMoveTopLevel => 'Oberste Ebene';
|
String get studysetMoveTopLevel => 'Oberste Ebene';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveCurrent => 'Aktueller Ort';
|
String get studysetMoveCurrent => '(aktuell)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveTopLevelRow => 'Oberste Ebene (in keinem Lernset)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveArchived => 'Archiviert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveNew => 'Neues Lernset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveSelectDestination => 'Wähle ein Ziel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String studysetMoveDestination(String name) {
|
||||||
|
return 'Ziel: $name';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveConfirm => 'Verschieben';
|
String get studysetMoveConfirm => 'Verschieben';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveShowInside => 'Inhalt anzeigen';
|
String get studysetMoveShowInside => 'Unter-Lernsets anzeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveHideInside => 'Inhalt ausblenden';
|
String get studysetMoveHideInside => 'Unter-Lernsets ausblenden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String studysetMoved(String destination) {
|
String studysetMoved(String title, String destination) {
|
||||||
return 'Nach „$destination“ verschoben.';
|
return '„$title“ nach $destination verschoben.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -877,11 +911,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get studysetSheetDescription => 'Beschreibung';
|
String get studysetSheetDescription => 'Beschreibung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetDescriptionAdd => 'Beschreibung hinzufügen';
|
String get studysetSheetDescriptionAdd =>
|
||||||
|
'Tippen, um eine Beschreibung hinzuzufügen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetInheritColor =>
|
String get studysetSheetDescriptionAdded =>
|
||||||
'Gleiche Farbe wie das Lernset, in dem es liegt';
|
'Beschreibung hinzugefügt — zum Bearbeiten tippen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetSheetInheritColor => 'Erben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fieldRequired => 'Pflichtfeld';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exploreTitle => 'Entdecken';
|
String get exploreTitle => 'Entdecken';
|
||||||
|
|
@ -2179,11 +2220,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get homeAllHidden => 'Alle Widgets sind ausgeblendet';
|
String get homeAllHidden => 'Alle Widgets sind ausgeblendet';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeAllHiddenHint =>
|
String get homeAllHiddenBody =>
|
||||||
'Halte hier lange gedrückt, um welche zurückzuholen.';
|
'Blende einige wieder ein, um dein Dashboard zu füllen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetGoal => 'Lernziel';
|
String get homeCustomise => 'Anpassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeWidgetGoal => 'Tagesziel';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetWeek => 'Wochenübersicht';
|
String get homeWidgetWeek => 'Wochenübersicht';
|
||||||
|
|
@ -2299,17 +2343,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get weekRestDay => 'Ruhetag';
|
String get weekRestDay => 'Ruhetag';
|
||||||
|
|
||||||
@override
|
|
||||||
String weekDueCount(int count) {
|
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
|
||||||
count,
|
|
||||||
locale: localeName,
|
|
||||||
other: '$count Karten fällig',
|
|
||||||
one: '1 Karte fällig',
|
|
||||||
);
|
|
||||||
return '$_temp0';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get heatmapLess => 'Weniger';
|
String get heatmapLess => 'Weniger';
|
||||||
|
|
||||||
|
|
@ -2334,7 +2367,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: '$count Aktivitäten',
|
other: '$count Aktivitäten',
|
||||||
one: '1 Aktivität',
|
one: '1 Aktivität',
|
||||||
zero: 'Nichts',
|
|
||||||
);
|
);
|
||||||
return '$_temp0 · $date';
|
return '$_temp0 · $date';
|
||||||
}
|
}
|
||||||
|
|
@ -3167,7 +3199,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get imageOpenPdfPage => 'PDF auf dieser Seite öffnen';
|
String get imageOpenPdfPage => 'PDF auf dieser Seite öffnen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetDeadlines => 'Lesefristen';
|
String get homeWidgetDeadlines => 'PDF-Lesefristen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deadlinesTitle => 'Lesefristen';
|
String get deadlinesTitle => 'Lesefristen';
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetSecondaryHelper =>
|
String get studysetSheetSecondaryHelper =>
|
||||||
'Shown as a small label above the title.';
|
'Shown as a small label above the title on cards and the studyset\'s header. Paths use it to label tiers (Challenge, Milestone, Lesson).';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetColor => 'Colour';
|
String get studysetSheetColor => 'Colour';
|
||||||
|
|
@ -169,6 +169,23 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String studysetCountFiles(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count files',
|
||||||
|
one: '1 file',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get libraryRefresh => 'Refresh';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get libraryRetry => 'Retry';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetNoContent => 'No content yet';
|
String get studysetNoContent => 'No content yet';
|
||||||
|
|
||||||
|
|
@ -840,20 +857,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get studysetMoveTopLevel => 'Top level';
|
String get studysetMoveTopLevel => 'Top level';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveCurrent => 'Where it is now';
|
String get studysetMoveCurrent => '(current)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveTopLevelRow => 'Top level (no parent)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveArchived => 'Archived';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveNew => 'New studyset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetMoveSelectDestination => 'Select a destination';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String studysetMoveDestination(String name) {
|
||||||
|
return 'Destination: $name';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveConfirm => 'Move';
|
String get studysetMoveConfirm => 'Move';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveShowInside => 'Show what is inside';
|
String get studysetMoveShowInside => 'Show sub-studysets';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetMoveHideInside => 'Hide what is inside';
|
String get studysetMoveHideInside => 'Hide sub-studysets';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String studysetMoved(String destination) {
|
String studysetMoved(String title, String destination) {
|
||||||
return 'Moved to $destination.';
|
return 'Moved \"$title\" to $destination.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -872,11 +906,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get studysetSheetDescription => 'Description';
|
String get studysetSheetDescription => 'Description';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetDescriptionAdd => 'Add a description';
|
String get studysetSheetDescriptionAdd => 'Tap to add a description';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get studysetSheetInheritColor =>
|
String get studysetSheetDescriptionAdded => 'Description added — tap to edit';
|
||||||
'Same colour as the studyset it is in';
|
|
||||||
|
@override
|
||||||
|
String get studysetSheetInheritColor => 'Inherit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fieldRequired => 'required';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exploreTitle => 'Explore';
|
String get exploreTitle => 'Explore';
|
||||||
|
|
@ -2166,10 +2205,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get homeAllHidden => 'All widgets are hidden';
|
String get homeAllHidden => 'All widgets are hidden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeAllHiddenHint => 'Long-press here to bring some back.';
|
String get homeAllHiddenBody => 'Bring some back to fill your dashboard.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetGoal => 'Study goal';
|
String get homeCustomise => 'Customise';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeWidgetGoal => 'Daily goal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetWeek => 'Week overview';
|
String get homeWidgetWeek => 'Week overview';
|
||||||
|
|
@ -2283,17 +2325,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get weekRestDay => 'Rest day';
|
String get weekRestDay => 'Rest day';
|
||||||
|
|
||||||
@override
|
|
||||||
String weekDueCount(int count) {
|
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
|
||||||
count,
|
|
||||||
locale: localeName,
|
|
||||||
other: '$count cards due',
|
|
||||||
one: '1 card due',
|
|
||||||
);
|
|
||||||
return '$_temp0';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get heatmapLess => 'Less';
|
String get heatmapLess => 'Less';
|
||||||
|
|
||||||
|
|
@ -2318,7 +2349,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: '$count activities',
|
other: '$count activities',
|
||||||
one: '1 activity',
|
one: '1 activity',
|
||||||
zero: 'Nothing',
|
|
||||||
);
|
);
|
||||||
return '$_temp0 · $date';
|
return '$_temp0 · $date';
|
||||||
}
|
}
|
||||||
|
|
@ -3140,7 +3170,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get imageOpenPdfPage => 'Open the PDF at this page';
|
String get imageOpenPdfPage => 'Open the PDF at this page';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetDeadlines => 'Reading deadlines';
|
String get homeWidgetDeadlines => 'PDF reading deadlines';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deadlinesTitle => 'Reading deadlines';
|
String get deadlinesTitle => 'Reading deadlines';
|
||||||
|
|
@ -3681,12 +3711,12 @@ class AppLocalizationsEnUs extends AppLocalizationsEn {
|
||||||
return 'Color: $color';
|
return 'Color: $color';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get studysetSheetInheritColor => 'Same color as the studyset it is in';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tagSheetColor => 'Color';
|
String get tagSheetColor => 'Color';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get formatAlignCenter => 'Center';
|
String get formatAlignCenter => 'Center';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeCustomise => 'Customize';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/sync/bloc/sync_status_bloc.dart';
|
||||||
|
|
||||||
|
/// Syncs now through [bloc] and completes once that sync is over, however it
|
||||||
|
/// went, or after [timeout]: what a pull to refresh shows its spinner for.
|
||||||
|
Future<void> syncAndWait(
|
||||||
|
SyncStatusBloc bloc, {
|
||||||
|
Duration timeout = const Duration(seconds: 30),
|
||||||
|
}) {
|
||||||
|
final done = Completer<void>();
|
||||||
|
var started = false;
|
||||||
|
final subscription = bloc.stream.listen((status) {
|
||||||
|
if (status.syncing) {
|
||||||
|
started = true;
|
||||||
|
} else if (started && !done.isCompleted) {
|
||||||
|
done.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bloc.add(const SyncStatusEventSyncRequested());
|
||||||
|
return done.future
|
||||||
|
.timeout(timeout, onTimeout: () {})
|
||||||
|
// Not waited for: a broadcast stream's cancel never completes under a
|
||||||
|
// widget test's fake clock.
|
||||||
|
.whenComplete(() => unawaited(subscription.cancel()));
|
||||||
|
}
|
||||||
71
halkyon_learn_flutter/lib/src/core/widget/count_stepper.dart
Normal file
71
halkyon_learn_flutter/lib/src/core/widget/count_stepper.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// A number between a minus and a plus, the width of what holds it, in an
|
||||||
|
/// outlined box: the number bold ([style], body text by default), [suffix]
|
||||||
|
/// (" / day") lighter after it. A null action can't go further.
|
||||||
|
class const CountStepper({
|
||||||
|
required final String value,
|
||||||
|
required final VoidCallback? onFewer,
|
||||||
|
required final VoidCallback? onMore,
|
||||||
|
|
||||||
|
/// Said after the number, lighter.
|
||||||
|
final String suffix = '',
|
||||||
|
|
||||||
|
/// The number's style, before it is made bold.
|
||||||
|
final TextStyle? style,
|
||||||
|
super.key,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.studyFewer,
|
||||||
|
onPressed: onFewer,
|
||||||
|
icon: const Icon(LucideIcons.minus),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: value,
|
||||||
|
children: [
|
||||||
|
if (suffix.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text: suffix,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: (style ?? theme.textTheme.bodyLarge)?.copyWith(
|
||||||
|
color: colors.onSurface,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.studyMore,
|
||||||
|
onPressed: onMore,
|
||||||
|
icon: const Icon(LucideIcons.plus),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
halkyon_learn_flutter/lib/src/core/widget/field_label.dart
Normal file
47
halkyon_learn_flutter/lib/src/core/widget/field_label.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// A field's label marked as required: [text] and a lighter asterisk, which
|
||||||
|
/// screen readers announce as "required".
|
||||||
|
class const RequiredFieldLabel(final String text, {super.key})
|
||||||
|
extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Semantics(
|
||||||
|
label: '$text, ${AppLocalizations.of(context).fieldRequired}',
|
||||||
|
excludeSemantics: true,
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: text,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: ' *',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A question mark at a field's end that explains it: [message] shows on a
|
||||||
|
/// tap (or hovering) and stays for a while to be read.
|
||||||
|
class const FieldInfoButton(final String message, {super.key})
|
||||||
|
extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Tooltip(
|
||||||
|
message: message,
|
||||||
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
showDuration: const Duration(seconds: 8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Icon(
|
||||||
|
LucideIcons.circleQuestionMark,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
/// A widget of the home dashboard.
|
/// A widget of the home dashboard, in the order a new dashboard shows them.
|
||||||
enum HomeWidget() {
|
enum HomeWidget() {
|
||||||
/// The Pomodoro timer.
|
|
||||||
pomodoro,
|
|
||||||
|
|
||||||
/// The study goal.
|
/// The study goal.
|
||||||
goal,
|
goal,
|
||||||
|
|
||||||
|
/// The Pomodoro timer.
|
||||||
|
pomodoro,
|
||||||
|
|
||||||
/// The reading deadlines of PDFs; it shows only while there are some.
|
/// The reading deadlines of PDFs; it shows only while there are some.
|
||||||
deadlines,
|
deadlines,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/home/widget/week_overview.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// A year of studying as a grid, like the contribution grids of code
|
/// A year of studying as a grid, like the contribution grids of code
|
||||||
/// hosts: a column per week, Monday to Sunday, each day as dark as what
|
/// hosts: a column per week, Monday to Sunday, each day as dark as what
|
||||||
/// was studied on it, with the months on top and every other weekday on
|
/// was studied on it, with the months on top and every other weekday on
|
||||||
/// the left. Centred when it fits, scrolling sideways otherwise, the latest
|
/// the left. Centred when it fits, scrolling sideways otherwise, weekdays
|
||||||
/// weeks in view first.
|
/// and all, the latest weeks in view first. Days to come are outlined.
|
||||||
class const ActivityHeatmap({
|
class const ActivityHeatmap({
|
||||||
/// What was studied, by day.
|
/// What was studied, by day.
|
||||||
required final StudyActivity activity,
|
required final StudyActivity activity,
|
||||||
|
|
@ -104,45 +105,32 @@ class const ActivityHeatmap({
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
for (final monday in mondays)
|
for (final monday in mondays)
|
||||||
Padding(
|
Column(
|
||||||
padding: const EdgeInsets.only(right: _gap),
|
children: [
|
||||||
child: Column(
|
for (var row = 0; row < 7; row++)
|
||||||
children: [
|
_cellFor(context, addDays(monday, row)),
|
||||||
for (var row = 0; row < 7; row++)
|
],
|
||||||
_cellFor(context, addDays(monday, row)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
final grid = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [weekdays, const SizedBox(width: 6), weeksGrid],
|
||||||
|
);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// The weekdays stay in view while the weeks scroll.
|
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) =>
|
builder: (context, constraints) =>
|
||||||
_weekdaysWidth + 6 + weeks * _step <= constraints.maxWidth
|
_weekdaysWidth + 6 + weeks * _step <= constraints.maxWidth
|
||||||
? Center(
|
? Center(child: grid)
|
||||||
child: Row(
|
: SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
scrollDirection: Axis.horizontal,
|
||||||
children: [weekdays, const SizedBox(width: 6), weeksGrid],
|
reverse: true,
|
||||||
),
|
child: grid,
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
weekdays,
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
reverse: true,
|
|
||||||
child: weeksGrid,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
@ -150,18 +138,18 @@ class const ActivityHeatmap({
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(l10n.heatmapTotal(total), style: muted)),
|
Expanded(child: Text(l10n.heatmapTotal(total), style: muted)),
|
||||||
Text(l10n.heatmapLess, style: muted),
|
Text(l10n.heatmapLess, style: muted),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 6),
|
||||||
for (var level = 0; level <= 4; level++)
|
for (var level = 0; level <= 4; level++)
|
||||||
Container(
|
Container(
|
||||||
width: 10,
|
width: 12,
|
||||||
height: 10,
|
height: 12,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 1.5),
|
margin: const EdgeInsets.symmetric(horizontal: 1.5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorOf(theme, level),
|
color: colorOf(theme, level),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 6),
|
||||||
Text(l10n.heatmapMore, style: muted),
|
Text(l10n.heatmapMore, style: muted),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -173,13 +161,23 @@ class const ActivityHeatmap({
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final future = day.isAfter(today);
|
final future = day.isAfter(today);
|
||||||
final count = activity.on(day);
|
final count = activity.on(day);
|
||||||
final cell = Container(
|
// Centred in its slot of the grid, the gap shared on every side.
|
||||||
width: _cell,
|
final cell = Padding(
|
||||||
height: _cell,
|
padding: const EdgeInsets.all(_gap / 2),
|
||||||
margin: const EdgeInsets.only(bottom: _gap),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
width: _cell,
|
||||||
color: future ? Colors.transparent : colorOf(theme, heatLevel(count)),
|
height: _cell,
|
||||||
borderRadius: BorderRadius.circular(3),
|
decoration: BoxDecoration(
|
||||||
|
color: future ? Colors.transparent : colorOf(theme, heatLevel(count)),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
border: future
|
||||||
|
? Border.all(
|
||||||
|
color: theme.colorScheme.outlineVariant.withValues(
|
||||||
|
alpha: 0.4,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (future) {
|
if (future) {
|
||||||
|
|
@ -189,7 +187,8 @@ class const ActivityHeatmap({
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: AppLocalizations.of(
|
message: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).heatmapDay(count, DateFormat.yMMMd(locale).format(day)),
|
).heatmapDay(count, weekdayAndDate(locale, day)),
|
||||||
|
waitDuration: const Duration(milliseconds: 300),
|
||||||
child: cell,
|
child: cell,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_goal.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_goal.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The study goal: a ring of how far the period got, with what is still to
|
/// The study goal: a ring of how far the period got, with what is still to
|
||||||
/// go; without a goal, an invitation to set one. Tapping opens the goal's
|
/// go and a pencil to change it; without a goal, an invitation to set one.
|
||||||
/// sheet.
|
/// Tapping opens the goal's sheet.
|
||||||
class const GoalCard({
|
class const GoalCard({
|
||||||
/// The goal; null without one.
|
/// The goal; null without one.
|
||||||
required final StudyGoal? goal,
|
required final StudyGoal? goal,
|
||||||
|
|
@ -25,13 +25,14 @@ class const GoalCard({
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
final goal = this.goal;
|
final goal = this.goal;
|
||||||
final muted = theme.textTheme.bodyMedium?.copyWith(
|
final small = theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: colors.onSurfaceVariant,
|
||||||
);
|
);
|
||||||
return SectionCard(
|
return Card(
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await showAdaptiveSheet<_GoalResult>(
|
final result = await showAdaptiveSheet<_GoalResult>(
|
||||||
context,
|
context,
|
||||||
|
|
@ -46,7 +47,7 @@ class const GoalCard({
|
||||||
child: goal == null
|
child: goal == null
|
||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(LucideIcons.target, color: theme.colorScheme.primary),
|
Icon(LucideIcons.target, color: colors.primary),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -57,10 +58,11 @@ class const GoalCard({
|
||||||
style: theme.textTheme.titleMedium,
|
style: theme.textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(l10n.goalEmptySubtitle, style: muted),
|
Text(l10n.goalEmptySubtitle, style: small),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
const Icon(LucideIcons.chevronRight),
|
const Icon(LucideIcons.chevronRight),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -86,26 +88,39 @@ class const GoalCard({
|
||||||
progress,
|
progress,
|
||||||
goal.target,
|
goal.target,
|
||||||
),
|
),
|
||||||
style: muted,
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
if (progress >= goal.target)
|
if (progress >= goal.target)
|
||||||
Text(
|
Row(
|
||||||
l10n.goalMet,
|
children: [
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
Icon(
|
||||||
color: theme.colorScheme.primary,
|
LucideIcons.circleCheck,
|
||||||
fontWeight: FontWeight.w600,
|
size: 16,
|
||||||
),
|
color: colors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
l10n.goalMet,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
l10n.goalRemaining(goal.target - progress),
|
l10n.goalRemaining(goal.target - progress),
|
||||||
style: theme.textTheme.bodySmall,
|
style: small,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(LucideIcons.chevronRight),
|
const SizedBox(width: 8),
|
||||||
|
Icon(LucideIcons.pencil, size: 18, color: colors.outline),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -120,20 +135,25 @@ class const _Ring({required final int progress, required final int target})
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return SizedBox(
|
return SizedBox.square(
|
||||||
width: 56,
|
dimension: 56,
|
||||||
height: 56,
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox.expand(
|
SizedBox.expand(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: math.min(1, progress / target),
|
value: math.min(1, progress / target),
|
||||||
strokeWidth: 6,
|
strokeWidth: 5,
|
||||||
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$progress',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('$progress', style: theme.textTheme.titleMedium),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -143,8 +163,8 @@ class const _Ring({required final int progress, required final int target})
|
||||||
/// What the goal sheet ends with: the goal set, or null once removed.
|
/// What the goal sheet ends with: the goal set, or null once removed.
|
||||||
typedef _GoalResult = ({StudyGoal? goal});
|
typedef _GoalResult = ({StudyGoal? goal});
|
||||||
|
|
||||||
/// Setting the study goal: a day or a week, and how many cards; removing an
|
/// Setting the study goal: a day or a week, and how many cards; saving
|
||||||
/// existing one.
|
/// across the sheet's width, and removing an existing one below it.
|
||||||
class const _GoalSheet({required final StudyGoal? initial})
|
class const _GoalSheet({required final StudyGoal? initial})
|
||||||
extends StatefulWidget {
|
extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -166,29 +186,35 @@ class _GoalSheetState() extends State<_GoalSheet> {
|
||||||
final daily = _period == GoalPeriod.day;
|
final daily = _period == GoalPeriod.day;
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
title: widget.initial == null ? l10n.goalSetTitle : l10n.goalEditTitle,
|
title: widget.initial == null ? l10n.goalSetTitle : l10n.goalEditTitle,
|
||||||
footer: Row(
|
footer: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (widget.initial != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop((goal: null)),
|
|
||||||
child: Text(l10n.goalRemove),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.of(
|
onPressed: () => Navigator.of(
|
||||||
context,
|
context,
|
||||||
).pop((goal: StudyGoal(period: _period, target: _target))),
|
).pop((goal: StudyGoal(period: _period, target: _target))),
|
||||||
child: Text(l10n.goalSave),
|
child: Text(l10n.goalSave),
|
||||||
),
|
),
|
||||||
|
if (widget.initial != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop((goal: null)),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
child: Text(l10n.goalRemove),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
daily ? l10n.goalDescriptionDaily : l10n.goalDescriptionWeekly,
|
daily ? l10n.goalDescriptionDaily : l10n.goalDescriptionWeekly,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -207,7 +233,6 @@ class _GoalSheetState() extends State<_GoalSheet> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
selected: {_period},
|
selected: {_period},
|
||||||
showSelectedIcon: false,
|
|
||||||
onSelectionChanged: (selection) =>
|
onSelectionChanged: (selection) =>
|
||||||
setState(() => _period = selection.single),
|
setState(() => _period = selection.single),
|
||||||
),
|
),
|
||||||
|
|
@ -217,33 +242,16 @@ class _GoalSheetState() extends State<_GoalSheet> {
|
||||||
style: theme.textTheme.titleSmall,
|
style: theme.textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
CountStepper(
|
||||||
children: [
|
value: '$_target',
|
||||||
IconButton.outlined(
|
style: theme.textTheme.titleMedium,
|
||||||
tooltip: l10n.studyFewer,
|
onFewer: _target > StudyGoal.minTarget
|
||||||
onPressed: _target > StudyGoal.minTarget
|
? () => _setTarget(_target - 1)
|
||||||
? () => _setTarget(_target - 1)
|
: null,
|
||||||
: null,
|
onMore: _target < StudyGoal.maxTarget
|
||||||
icon: const Icon(LucideIcons.minus),
|
? () => _setTarget(_target + 1)
|
||||||
),
|
: null,
|
||||||
SizedBox(
|
|
||||||
width: 96,
|
|
||||||
child: Text(
|
|
||||||
'$_target',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton.outlined(
|
|
||||||
tooltip: l10n.studyMore,
|
|
||||||
onPressed: _target < StudyGoal.maxTarget
|
|
||||||
? () => _setTarget(_target + 1)
|
|
||||||
: null,
|
|
||||||
icon: const Icon(LucideIcons.plus),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
|
@ -7,7 +8,6 @@ import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/sync/widget/app_mismatch_banner.dart';
|
import 'package:halkyon_learn_flutter/src/core/sync/widget/app_mismatch_banner.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/sync/widget/sync_trouble_badge.dart';
|
import 'package:halkyon_learn_flutter/src/core/sync/widget/sync_trouble_badge.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/bloc/home_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/bloc/home_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/home_layout.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/home_layout.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
||||||
|
|
@ -55,10 +55,17 @@ class _HomeState() extends State<_Home> {
|
||||||
|
|
||||||
void _send(HomeEvent event) => context.read<HomeBloc>().add(event);
|
void _send(HomeEvent event) => context.read<HomeBloc>().add(event);
|
||||||
|
|
||||||
|
/// Starts arranging the dashboard, with a tap the hand can feel.
|
||||||
|
void _arrange() {
|
||||||
|
if (!_arranging) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
setState(() => _arranging = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
|
||||||
return BlocBuilder<HomeBloc, HomeState>(
|
return BlocBuilder<HomeBloc, HomeState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final loaded = switch (state) {
|
final loaded = switch (state) {
|
||||||
|
|
@ -74,7 +81,6 @@ class _HomeState() extends State<_Home> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: theme.colorScheme.surfaceContainerLow,
|
|
||||||
appBar: _arranging
|
appBar: _arranging
|
||||||
? AppBar(
|
? AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
|
|
@ -112,7 +118,7 @@ class _HomeState() extends State<_Home> {
|
||||||
tooltip: l10n.settingsTitle,
|
tooltip: l10n.settingsTitle,
|
||||||
onPressed: () => context.push(RoutePaths.settings),
|
onPressed: () => context.push(RoutePaths.settings),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
|
@ -142,29 +148,34 @@ class _HomeState() extends State<_Home> {
|
||||||
onChanged: (goal) => _send(HomeEventGoalSet(goal)),
|
onChanged: (goal) => _send(HomeEventGoalSet(goal)),
|
||||||
),
|
),
|
||||||
HomeWidget.deadlines => ReadingDeadlinesCard(placeholder: _arranging),
|
HomeWidget.deadlines => ReadingDeadlinesCard(placeholder: _arranging),
|
||||||
HomeWidget.week => SectionCard(
|
HomeWidget.week => Card(
|
||||||
padding: const EdgeInsets.fromLTRB(4, 12, 4, 12),
|
child: Padding(
|
||||||
child: WeekOverview(
|
padding: const EdgeInsets.fromLTRB(4, 12, 4, 12),
|
||||||
selected: selected,
|
child: WeekOverview(
|
||||||
today: today,
|
selected: selected,
|
||||||
week: weekOf(selected, state.activity, state.restDays, state.now),
|
today: today,
|
||||||
due: state.due,
|
week: weekOf(selected, state.activity, state.restDays, state.now),
|
||||||
restDays: state.restDays,
|
due: state.due,
|
||||||
onSelect: (day) => setState(() => _selected = day),
|
restDays: state.restDays,
|
||||||
onToggleRestDay: () => _send(HomeEventRestDayToggled(selected)),
|
onSelect: (day) => setState(() => _selected = day),
|
||||||
|
onToggleRestDay: () => _send(HomeEventRestDayToggled(selected)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
HomeWidget.activity => SectionCard(
|
HomeWidget.activity => Card(
|
||||||
child: Column(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
child: Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
l10n.homeActivityTitle,
|
children: [
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
Text(
|
||||||
),
|
l10n.homeActivityTitle,
|
||||||
const SizedBox(height: 12),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
ActivityHeatmap(activity: state.activity, today: today),
|
),
|
||||||
],
|
const SizedBox(height: 12),
|
||||||
|
ActivityHeatmap(activity: state.activity, today: today),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -180,36 +191,24 @@ class _HomeState() extends State<_Home> {
|
||||||
onChanged: (layout) => _send(HomeEventLayoutChanged(layout)),
|
onChanged: (layout) => _send(HomeEventLayoutChanged(layout)),
|
||||||
)
|
)
|
||||||
: layout.shown.isEmpty
|
: layout.shown.isEmpty
|
||||||
? GestureDetector(
|
? _AllHidden(onArrange: _arrange)
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onLongPress: () => setState(() => _arranging = true),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l10n.homeAllHidden,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(l10n.homeAllHiddenHint),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView(
|
: ListView(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
for (final widget in layout.shown)
|
for (final widget in layout.shown)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
key: ValueKey(widget),
|
key: ValueKey(widget),
|
||||||
// Taps still reach the widget's own controls.
|
// Taps still reach the widget's own controls.
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onLongPress: () => setState(() => _arranging = true),
|
onLongPress: _arrange,
|
||||||
child: Padding(
|
// The reading deadlines leave no gap while they show
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
// nothing: they keep their own.
|
||||||
child: section(widget),
|
child: widget == HomeWidget.deadlines
|
||||||
),
|
? section(widget)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: section(widget),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -218,9 +217,51 @@ class _HomeState() extends State<_Home> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// With every widget hidden: what happened, and a way back to arranging.
|
||||||
|
class const _AllHidden({required final VoidCallback onArrange})
|
||||||
|
extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
LucideIcons.layoutGrid,
|
||||||
|
size: 40,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(l10n.homeAllHidden, style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
l10n.homeAllHiddenBody,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
onPressed: onArrange,
|
||||||
|
icon: const Icon(LucideIcons.eye, size: 18),
|
||||||
|
label: Text(l10n.homeCustomise),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The dashboard being arranged: every widget, hidden ones greyed, each
|
/// The dashboard being arranged: every widget, hidden ones greyed, each
|
||||||
/// with a handle to drag it elsewhere and an eye to hide or show it; the
|
/// with a handle beside it to drag it elsewhere and an eye on its corner to
|
||||||
/// widgets themselves don't react meanwhile.
|
/// hide or show it; the widgets themselves don't react meanwhile. A widget
|
||||||
|
/// picked up lifts a little, with a tap the hand can feel.
|
||||||
class const _Arranging({
|
class const _Arranging({
|
||||||
required final HomeLayout layout,
|
required final HomeLayout layout,
|
||||||
required final Widget Function(HomeWidget widget) section,
|
required final Widget Function(HomeWidget widget) section,
|
||||||
|
|
@ -229,16 +270,35 @@ class const _Arranging({
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
String label(HomeWidget widget) => switch (widget) {
|
String label(HomeWidget widget) => switch (widget) {
|
||||||
HomeWidget.pomodoro => l10n.homeWidgetPomodoro,
|
HomeWidget.pomodoro => l10n.pomodoroTitle,
|
||||||
HomeWidget.goal => l10n.homeWidgetGoal,
|
HomeWidget.goal => l10n.homeWidgetGoal,
|
||||||
HomeWidget.deadlines => l10n.homeWidgetDeadlines,
|
HomeWidget.deadlines => l10n.homeWidgetDeadlines,
|
||||||
HomeWidget.week => l10n.homeWidgetWeek,
|
HomeWidget.week => l10n.homeWidgetWeek,
|
||||||
HomeWidget.activity => l10n.homeWidgetActivity,
|
HomeWidget.activity => l10n.homeWidgetActivity,
|
||||||
};
|
};
|
||||||
return ReorderableListView(
|
return ReorderableListView(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(16),
|
||||||
buildDefaultDragHandles: false,
|
buildDefaultDragHandles: false,
|
||||||
|
onReorderStart: (_) => HapticFeedback.mediumImpact(),
|
||||||
|
proxyDecorator: (child, index, animation) => AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
child: child,
|
||||||
|
builder: (context, child) {
|
||||||
|
final lift = Curves.easeInOut.transform(animation.value);
|
||||||
|
return Transform.scale(
|
||||||
|
scale: 1 + 0.03 * lift,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
elevation: 8 * lift,
|
||||||
|
shadowColor: theme.colorScheme.shadow,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
onReorderItem: (from, to) {
|
onReorderItem: (from, to) {
|
||||||
final order = [...layout.order];
|
final order = [...layout.order];
|
||||||
final moved = order.removeAt(from);
|
final moved = order.removeAt(from);
|
||||||
|
|
@ -249,42 +309,53 @@ class const _Arranging({
|
||||||
for (final (i, widget) in layout.order.indexed)
|
for (final (i, widget) in layout.order.indexed)
|
||||||
Padding(
|
Padding(
|
||||||
key: ValueKey(widget),
|
key: ValueKey(widget),
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
ReorderableDragStartListener(
|
ReorderableDragStartListener(
|
||||||
index: i,
|
index: i,
|
||||||
child: Tooltip(
|
child: IconButton(
|
||||||
message: l10n.homeDragToReorder,
|
tooltip: l10n.homeDragToReorder,
|
||||||
child: const Padding(
|
onPressed: () {},
|
||||||
padding: EdgeInsets.all(12),
|
icon: Icon(
|
||||||
child: Icon(LucideIcons.gripVertical),
|
LucideIcons.gripVertical,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Opacity(
|
child: Stack(
|
||||||
opacity: layout.hidden.contains(widget) ? 0.4 : 1,
|
children: [
|
||||||
child: IgnorePointer(child: section(widget)),
|
// Room at the top end for the eye on the corner.
|
||||||
),
|
Padding(
|
||||||
),
|
padding: const EdgeInsetsDirectional.only(
|
||||||
IconButton(
|
top: 8,
|
||||||
tooltip: layout.hidden.contains(widget)
|
end: 8,
|
||||||
? l10n.homeShowWidget(label(widget))
|
),
|
||||||
: l10n.homeHideWidget(label(widget)),
|
child: IgnorePointer(
|
||||||
icon: Icon(
|
child: Opacity(
|
||||||
layout.hidden.contains(widget)
|
opacity: layout.hidden.contains(widget) ? 0.4 : 1,
|
||||||
? LucideIcons.eyeOff
|
child: section(widget),
|
||||||
: LucideIcons.eye,
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => onChanged(
|
),
|
||||||
HomeLayout(
|
PositionedDirectional(
|
||||||
order: layout.order,
|
top: 0,
|
||||||
hidden: layout.hidden.contains(widget)
|
end: 0,
|
||||||
? ({...layout.hidden}..remove(widget))
|
child: _EyeBadge(
|
||||||
: {...layout.hidden, widget},
|
hidden: layout.hidden.contains(widget),
|
||||||
),
|
label: label(widget),
|
||||||
|
onTap: () => onChanged(
|
||||||
|
HomeLayout(
|
||||||
|
order: layout.order,
|
||||||
|
hidden: layout.hidden.contains(widget)
|
||||||
|
? ({...layout.hidden}..remove(widget))
|
||||||
|
: {...layout.hidden, widget},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -294,3 +365,41 @@ class const _Arranging({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The eye on a widget's corner while arranging: open while it shows,
|
||||||
|
/// crossed out while hidden; a tap turns it.
|
||||||
|
class const _EyeBadge({
|
||||||
|
required final bool hidden,
|
||||||
|
required final String label,
|
||||||
|
required final VoidCallback onTap,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final tooltip = hidden
|
||||||
|
? l10n.homeShowWidget(label)
|
||||||
|
: l10n.homeHideWidget(label);
|
||||||
|
return Tooltip(
|
||||||
|
message: tooltip,
|
||||||
|
child: Material(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
elevation: 1,
|
||||||
|
child: InkWell(
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child: Icon(
|
||||||
|
hidden ? LucideIcons.eyeOff : LucideIcons.eye,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
semanticLabel: tooltip,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,35 +25,37 @@ class const StreakChip({
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final lit = streak > 0;
|
final lit = streak > 0;
|
||||||
return Tooltip(
|
return Center(
|
||||||
message: AppLocalizations.of(context).homeStreakTooltip,
|
child: Tooltip(
|
||||||
child: InkWell(
|
message: AppLocalizations.of(context).homeStreakTooltip,
|
||||||
borderRadius: BorderRadius.circular(20),
|
child: InkWell(
|
||||||
onTap: () => showAdaptiveSheet<void>(
|
borderRadius: BorderRadius.circular(20),
|
||||||
context,
|
onTap: () => showAdaptiveSheet<void>(
|
||||||
builder: (context) =>
|
context,
|
||||||
_StreakSheet(streak: streak, week: week, today: today),
|
builder: (context) =>
|
||||||
),
|
_StreakSheet(streak: streak, week: week, today: today),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
child: Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Icon(
|
children: [
|
||||||
LucideIcons.flame,
|
Icon(
|
||||||
size: 20,
|
LucideIcons.flame,
|
||||||
color: lit
|
size: 20,
|
||||||
? streakFlameColor
|
color: lit
|
||||||
: theme.colorScheme.onSurfaceVariant,
|
? streakFlameColor
|
||||||
),
|
: theme.colorScheme.onSurfaceVariant,
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'$streak',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 4),
|
||||||
],
|
Text(
|
||||||
|
'$streak',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -61,8 +63,10 @@ class const StreakChip({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The streak at large: a flame, how many days, what keeps it going, and
|
/// The streak at large: a big flame with the days inside, what keeps it
|
||||||
/// this week's days.
|
/// going, this week's days as round marks over their initials, and a word
|
||||||
|
/// on what to do today in a box below. No title: the flame says it; a close
|
||||||
|
/// button on the top end.
|
||||||
class const _StreakSheet({
|
class const _StreakSheet({
|
||||||
required final int streak,
|
required final int streak,
|
||||||
required final List<WeekDay> week,
|
required final List<WeekDay> week,
|
||||||
|
|
@ -72,162 +76,161 @@ class const _StreakSheet({
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
final studiedToday = week.any(
|
final studiedToday = week.any(
|
||||||
(day) => day.state == DayState.studied && day.day == today,
|
(day) => day.state == DayState.studied && day.day == today,
|
||||||
);
|
);
|
||||||
return SheetScaffold(
|
return SafeArea(
|
||||||
title: l10n.homeStreakTooltip,
|
child: Padding(
|
||||||
body: Column(
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||||
children: [
|
child: Column(
|
||||||
const SizedBox(height: 8),
|
mainAxisSize: MainAxisSize.min,
|
||||||
Icon(
|
children: [
|
||||||
LucideIcons.flame,
|
Align(
|
||||||
size: 64,
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
color: streak > 0
|
child: IconButton(
|
||||||
? streakFlameColor
|
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
|
||||||
: theme.colorScheme.onSurfaceVariant,
|
icon: const Icon(LucideIcons.x),
|
||||||
),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Text(l10n.streakDays(streak), style: theme.textTheme.headlineSmall),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
streak == 0 ? l10n.streakStart : l10n.streakGrow,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox.square(
|
||||||
const SizedBox(height: 20),
|
dimension: 96,
|
||||||
Row(
|
child: Stack(
|
||||||
children: [
|
alignment: Alignment.center,
|
||||||
for (final day in week) Expanded(child: _StreakDay(day: day)),
|
children: [
|
||||||
],
|
Icon(
|
||||||
),
|
LucideIcons.flame,
|
||||||
const SizedBox(height: 16),
|
size: 88,
|
||||||
Text(
|
color: streak > 0
|
||||||
streak == 0
|
? streakFlameColor
|
||||||
? l10n.streakFirstDay
|
: colors.onSurfaceVariant,
|
||||||
: studiedToday
|
),
|
||||||
? l10n.streakComeBack
|
// In the flame's bulb.
|
||||||
: l10n.streakKeepAlive,
|
Padding(
|
||||||
textAlign: TextAlign.center,
|
padding: const EdgeInsets.only(top: 22),
|
||||||
style: theme.textTheme.bodySmall,
|
child: Text(
|
||||||
),
|
'$streak',
|
||||||
const SizedBox(height: 8),
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
],
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.streakDays(streak),
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
streak == 0 ? l10n.streakStart : l10n.streakGrow,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
for (final day in week)
|
||||||
|
_StreakDay(day: day, today: day.day == today),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
streak == 0
|
||||||
|
? l10n.streakFirstDay
|
||||||
|
: studiedToday
|
||||||
|
? l10n.streakComeBack
|
||||||
|
: l10n.streakKeepAlive,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class const _StreakDay({required final WeekDay day}) extends StatelessWidget {
|
/// A day of the streak's week: a round mark over the weekday's initial, a
|
||||||
|
/// flame where studied, a snowflake where rested, a cross where missed; a
|
||||||
|
/// ring with a flame for today not studied yet, a plain ring for a day to
|
||||||
|
/// come; nothing before the person's first activity.
|
||||||
|
class const _StreakDay({required final WeekDay day, required final bool today})
|
||||||
|
extends StatelessWidget {
|
||||||
|
static const _size = 34.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
final locale = Localizations.localeOf(context).toLanguageTag();
|
final locale = Localizations.localeOf(context).toLanguageTag();
|
||||||
|
Widget circle({Color? color, Border? border, Widget? child}) => Container(
|
||||||
|
width: _size,
|
||||||
|
height: _size,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
border: border,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
final mark = switch (day.state) {
|
||||||
|
DayState.studied => circle(
|
||||||
|
color: streakFlameColor.withValues(alpha: 0.15),
|
||||||
|
child: const Icon(LucideIcons.flame, size: 18, color: streakFlameColor),
|
||||||
|
),
|
||||||
|
DayState.rested => circle(
|
||||||
|
color: colors.tertiaryContainer,
|
||||||
|
child: Icon(
|
||||||
|
LucideIcons.snowflake,
|
||||||
|
size: 18,
|
||||||
|
color: colors.onTertiaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DayState.today => circle(
|
||||||
|
border: Border.all(color: colors.primary, width: 2),
|
||||||
|
child: Icon(LucideIcons.flame, size: 16, color: colors.primary),
|
||||||
|
),
|
||||||
|
DayState.upcoming => circle(
|
||||||
|
border: Border.all(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
|
DayState.missed => circle(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
child: Icon(LucideIcons.x, size: 16, color: colors.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
DayState.notStarted => circle(),
|
||||||
|
};
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
mark,
|
||||||
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
DateFormat.E(locale).format(day.day),
|
DateFormat('EEEEE', locale).format(day.day),
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
color: colors.onSurfaceVariant,
|
color: today ? colors.primary : colors.onSurfaceVariant,
|
||||||
|
fontWeight: today ? FontWeight.w700 : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
|
||||||
DayMarker(day: day, due: 0),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How a day stands, as a small round mark: a check for studied, a
|
|
||||||
/// snowflake for rested, a cross for missed, a dashed ring for today; a day
|
|
||||||
/// to come shows how many cards fall due, or a faint dashed ring; a day
|
|
||||||
/// before the person's first activity shows nothing.
|
|
||||||
class const DayMarker({
|
|
||||||
required final WeekDay day,
|
|
||||||
required final int due,
|
|
||||||
super.key,
|
|
||||||
}) extends StatelessWidget {
|
|
||||||
static const _size = 24.0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colors = theme.colorScheme;
|
|
||||||
Widget solid(Color color, Widget child) => Container(
|
|
||||||
width: _size,
|
|
||||||
height: _size,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
Widget dashed(Color color, {double width = 1.5}) => SizedBox(
|
|
||||||
width: _size,
|
|
||||||
height: _size,
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: _DashedCircle(color: color, strokeWidth: width),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return switch (day.state) {
|
|
||||||
DayState.studied => solid(
|
|
||||||
colors.primary.withValues(alpha: 0.15),
|
|
||||||
Icon(LucideIcons.check, size: 14, color: colors.primary),
|
|
||||||
),
|
|
||||||
DayState.rested => solid(
|
|
||||||
colors.tertiaryContainer,
|
|
||||||
Icon(
|
|
||||||
LucideIcons.snowflake,
|
|
||||||
size: 14,
|
|
||||||
color: colors.onTertiaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DayState.missed => solid(
|
|
||||||
colors.surfaceContainerHighest,
|
|
||||||
Icon(LucideIcons.x, size: 14, color: colors.onSurfaceVariant),
|
|
||||||
),
|
|
||||||
DayState.notStarted => const SizedBox.square(dimension: _size),
|
|
||||||
DayState.today => dashed(colors.primary, width: 2),
|
|
||||||
DayState.upcoming when due > 0 => solid(
|
|
||||||
colors.secondaryContainer,
|
|
||||||
Text(
|
|
||||||
due > 99 ? '99+' : '$due',
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: colors.onSecondaryContainer,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DayState.upcoming => dashed(colors.outlineVariant),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A dashed circle: twelve dashes, a little more line than gap.
|
|
||||||
class const _DashedCircle({
|
|
||||||
required final Color color,
|
|
||||||
required final double strokeWidth,
|
|
||||||
}) extends CustomPainter {
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = color
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeWidth = strokeWidth
|
|
||||||
..strokeCap = StrokeCap.round;
|
|
||||||
final rect = Rect.fromCircle(
|
|
||||||
center: size.center(Offset.zero),
|
|
||||||
radius: (size.shortestSide - strokeWidth) / 2,
|
|
||||||
);
|
|
||||||
const dashes = 12;
|
|
||||||
const sweep = 2 * 3.141592653589793 / dashes;
|
|
||||||
for (var i = 0; i < dashes; i++) {
|
|
||||||
canvas.drawArc(rect, i * sweep, sweep * 0.55, false, paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(_DashedCircle oldDelegate) =>
|
|
||||||
oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/layout/window_size.dart';
|
import 'package:halkyon_learn_flutter/src/core/layout/window_size.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/streak.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// [date] as its weekday in full and its day of the month with the month
|
||||||
|
/// short, in [locale]'s words and order: "Thursday, Oct 8", "Donnerstag,
|
||||||
|
/// 8. Okt.".
|
||||||
|
String weekdayAndDate(String locale, DateTime date) =>
|
||||||
|
'${DateFormat.EEEE(locale).format(date)}, '
|
||||||
|
'${DateFormat.MMMd(locale).format(date)}';
|
||||||
|
|
||||||
/// The week of [selected] day: on top the day named, making it a rest day
|
/// The week of [selected] day: on top the day named, making it a rest day
|
||||||
/// when it is today or to come and not studied (a chip, on a phone a
|
/// when it is today or to come and not studied (a chip, on a phone a
|
||||||
/// snowflake toggle for the room; decided 2026-10-07), and going a day back
|
/// snowflake toggle for the room; decided 2026-10-07), and going a day back
|
||||||
|
|
@ -53,7 +59,7 @@ class const WeekOverview({
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8),
|
padding: const EdgeInsets.only(left: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
DateFormat.MMMMEEEEd(locale).format(selected),
|
weekdayAndDate(locale, selected),
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
|
@ -86,9 +92,12 @@ class const WeekOverview({
|
||||||
onPressed: onToggleRestDay,
|
onPressed: onToggleRestDay,
|
||||||
),
|
),
|
||||||
if (selected != today)
|
if (selected != today)
|
||||||
TextButton(
|
Padding(
|
||||||
onPressed: () => onSelect(today),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
child: Text(l10n.weekToday),
|
child: TextButton(
|
||||||
|
onPressed: () => onSelect(today),
|
||||||
|
child: Text(l10n.weekToday),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: l10n.weekPreviousDay,
|
tooltip: l10n.weekPreviousDay,
|
||||||
|
|
@ -161,10 +170,7 @@ class const _Day({
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final mark = Tooltip(
|
final mark = _Marker(day: day, due: due);
|
||||||
message: due > 0 ? AppLocalizations.of(context).weekDueCount(due) : '',
|
|
||||||
child: DayMarker(day: day, due: due),
|
|
||||||
);
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
|
@ -200,3 +206,91 @@ class const _Day({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How a day stands, as a small round mark: a check for studied, a
|
||||||
|
/// snowflake for rested, a cross for missed, a dashed ring for today; a day
|
||||||
|
/// to come shows how many cards fall due, or a faint dashed ring; a day
|
||||||
|
/// before the person's first activity shows nothing.
|
||||||
|
class const _Marker({required final WeekDay day, required final int due})
|
||||||
|
extends StatelessWidget {
|
||||||
|
static const _size = 24.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
Widget solid(Color color, Widget child) => Container(
|
||||||
|
width: _size,
|
||||||
|
height: _size,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
Widget dashed(Color color, {double width = 1.5}) => SizedBox(
|
||||||
|
width: _size,
|
||||||
|
height: _size,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _DashedCircle(color: color, strokeWidth: width),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return switch (day.state) {
|
||||||
|
DayState.studied => solid(
|
||||||
|
colors.primary.withValues(alpha: 0.15),
|
||||||
|
Icon(LucideIcons.check, size: 14, color: colors.primary),
|
||||||
|
),
|
||||||
|
DayState.rested => solid(
|
||||||
|
colors.tertiaryContainer,
|
||||||
|
Icon(
|
||||||
|
LucideIcons.snowflake,
|
||||||
|
size: 14,
|
||||||
|
color: colors.onTertiaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DayState.missed => solid(
|
||||||
|
colors.surfaceContainerHighest,
|
||||||
|
Icon(LucideIcons.x, size: 14, color: colors.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
DayState.notStarted => const SizedBox.square(dimension: _size),
|
||||||
|
DayState.today => dashed(colors.primary, width: 2),
|
||||||
|
DayState.upcoming when due > 0 => solid(
|
||||||
|
colors.secondaryContainer,
|
||||||
|
Text(
|
||||||
|
due > 99 ? '99+' : '$due',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: colors.onSecondaryContainer,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DayState.upcoming => dashed(colors.outlineVariant),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dashed circle: twelve dashes, a little more line than gap.
|
||||||
|
class const _DashedCircle({
|
||||||
|
required final Color color,
|
||||||
|
required final double strokeWidth,
|
||||||
|
}) extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = color
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
final rect = Rect.fromCircle(
|
||||||
|
center: size.center(Offset.zero),
|
||||||
|
radius: (size.shortestSide - strokeWidth) / 2,
|
||||||
|
);
|
||||||
|
const dashes = 12;
|
||||||
|
const sweep = 2 * 3.141592653589793 / dashes;
|
||||||
|
for (var i = 0; i < dashes; i++) {
|
||||||
|
canvas.drawArc(rect, i * sweep, sweep * 0.55, false, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_DashedCircle oldDelegate) =>
|
||||||
|
oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/bloc/reading_deadlines_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/bloc/reading_deadlines_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/deadline_status.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/deadline_status.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/reading_deadlines.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/reading_deadlines.dart';
|
||||||
|
|
@ -12,8 +11,10 @@ import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// Home's reading deadlines: those running, the fewest days left first, each
|
/// Home's reading deadlines: those running, the fewest days left first, each
|
||||||
/// with the page to reach today; then those to come. A tap opens the
|
/// with the page to reach today; then those to come. A tap opens the
|
||||||
/// timeline. Without any it shows nothing, or while Home is arranged a word
|
/// timeline. Without any it shows nothing, and leaves no gap: it keeps the
|
||||||
/// on when it appears.
|
/// one below it itself. While Home is arranged ([placeholder]) it shows its
|
||||||
|
/// name and when it appears instead, so that it can be moved and hidden
|
||||||
|
/// whether there are deadlines or not.
|
||||||
class const ReadingDeadlinesCard({final bool placeholder = false, super.key})
|
class const ReadingDeadlinesCard({final bool placeholder = false, super.key})
|
||||||
extends StatelessWidget {
|
extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -28,83 +29,89 @@ class const ReadingDeadlinesCard({final bool placeholder = false, super.key})
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final deadlines = state.deadlines;
|
final deadlines = state.deadlines;
|
||||||
|
if (placeholder) {
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(LucideIcons.calendarClock),
|
||||||
|
title: Text(l10n.homeWidgetDeadlines),
|
||||||
|
subtitle: Text(l10n.deadlinesPlaceholder),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (deadlines.isEmpty) {
|
if (deadlines.isEmpty) {
|
||||||
return !placeholder
|
return const SizedBox.shrink();
|
||||||
? const SizedBox.shrink()
|
|
||||||
: SectionCard(
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(LucideIcons.calendarClock),
|
|
||||||
title: Text(l10n.deadlinesTitle),
|
|
||||||
subtitle: Text(l10n.deadlinesPlaceholder),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
TextStyle? heading() => theme.textTheme.labelMedium?.copyWith(
|
TextStyle? heading() => theme.textTheme.labelMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 1.2,
|
||||||
);
|
);
|
||||||
return SectionCard(
|
return Padding(
|
||||||
padding: EdgeInsets.zero,
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
child: InkWell(
|
child: Card(
|
||||||
borderRadius: BorderRadius.circular(16),
|
child: InkWell(
|
||||||
onTap: () => context.push(RoutePaths.readingTimeline),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
onTap: () => context.push(RoutePaths.readingTimeline),
|
||||||
padding: const EdgeInsets.all(16),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(16),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Row(
|
children: [
|
||||||
children: [
|
Row(
|
||||||
Icon(
|
children: [
|
||||||
LucideIcons.calendarClock,
|
Icon(
|
||||||
color: theme.colorScheme.primary,
|
LucideIcons.calendarClock,
|
||||||
),
|
color: theme.colorScheme.primary,
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.deadlinesTitle,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
Tooltip(
|
Expanded(
|
||||||
message: l10n.deadlinesOpenTimeline,
|
child: Text(
|
||||||
child: const Icon(LucideIcons.chevronRight),
|
l10n.deadlinesTitle,
|
||||||
),
|
style: theme.textTheme.titleMedium,
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
if (deadlines.current.isNotEmpty) ...[
|
Tooltip(
|
||||||
const SizedBox(height: 12),
|
message: l10n.deadlinesOpenTimeline,
|
||||||
Text(l10n.deadlinesCurrent.toUpperCase(), style: heading()),
|
child: const Icon(LucideIcons.chevronRight),
|
||||||
for (final deadline in deadlines.current)
|
),
|
||||||
_Current(deadline: deadline),
|
],
|
||||||
],
|
|
||||||
if (deadlines.upcoming.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
l10n.deadlinesUpcoming.toUpperCase(),
|
|
||||||
style: heading(),
|
|
||||||
),
|
),
|
||||||
for (final (:pdf, :status) in deadlines.upcoming)
|
if (deadlines.current.isNotEmpty) ...[
|
||||||
Padding(
|
const SizedBox(height: 12),
|
||||||
padding: const EdgeInsets.only(top: 8),
|
Text(
|
||||||
child: Row(
|
l10n.deadlinesCurrent.toUpperCase(),
|
||||||
children: [
|
style: heading(),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
pdf.content.title,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.deadlineStartsIn(status.daysUntilStart),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
for (final deadline in deadlines.current)
|
||||||
|
_Current(deadline: deadline),
|
||||||
|
],
|
||||||
|
if (deadlines.upcoming.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
l10n.deadlinesUpcoming.toUpperCase(),
|
||||||
|
style: heading(),
|
||||||
|
),
|
||||||
|
for (final (:pdf, :status) in deadlines.upcoming)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
pdf.content.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
l10n.deadlineStartsIn(status.daysUntilStart),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/bloc/pomodoro_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/bloc/pomodoro_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||||
|
|
@ -26,123 +25,128 @@ class const PomodoroCard({super.key}) extends StatelessWidget {
|
||||||
final run = state.run;
|
final run = state.run;
|
||||||
final profile = run?.profile ?? state.settings.selected;
|
final profile = run?.profile ?? state.settings.selected;
|
||||||
void send(PomodoroEvent event) => context.read<PomodoroBloc>().add(event);
|
void send(PomodoroEvent event) => context.read<PomodoroBloc>().add(event);
|
||||||
return SectionCard(
|
return Card(
|
||||||
child: Column(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Row(
|
||||||
child: Text(
|
children: [
|
||||||
l10n.pomodoroTitle,
|
Expanded(
|
||||||
style: theme.textTheme.titleMedium,
|
child: Text(
|
||||||
|
l10n.pomodoroTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (run == null)
|
||||||
if (run == null)
|
PopupMenuButton<String>(
|
||||||
PopupMenuButton<String>(
|
tooltip: l10n.pomodoroChooseProfile,
|
||||||
tooltip: l10n.pomodoroChooseProfile,
|
initialValue: profile.id,
|
||||||
initialValue: profile.id,
|
onSelected: (id) => send(PomodoroEventProfileChosen(id)),
|
||||||
onSelected: (id) => send(PomodoroEventProfileChosen(id)),
|
itemBuilder: (context) => [
|
||||||
itemBuilder: (context) => [
|
for (final each in state.settings.profiles)
|
||||||
for (final each in state.settings.profiles)
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
value: each.id,
|
||||||
value: each.id,
|
child: Text(l10n.pomodoroProfileName(each)),
|
||||||
child: Text(l10n.pomodoroProfileName(each)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l10n.pomodoroProfileName(profile),
|
|
||||||
style: theme.textTheme.labelLarge,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
],
|
||||||
const Icon(LucideIcons.chevronDown, size: 18),
|
child: Padding(
|
||||||
],
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.pomodoroProfileName(profile),
|
||||||
|
style: theme.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(LucideIcons.chevronDown, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Text(
|
|
||||||
l10n.pomodoroProfileName(profile),
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: l10n.pomodoroProfiles,
|
|
||||||
icon: const Icon(LucideIcons.settings2),
|
|
||||||
onPressed: () => showPomodoroProfilesSheet(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Center(
|
|
||||||
child: PomodoroCountdown(
|
|
||||||
run: run ?? PomodoroRun.ready(profile),
|
|
||||||
style: theme.textTheme.displayMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Text(
|
|
||||||
switch (run) {
|
|
||||||
null => l10n.pomodoroSummaryOf(profile),
|
|
||||||
final run when !run.isRunning =>
|
|
||||||
'${l10n.pomodoroPhaseTitle(run)} · ${l10n.pomodoroPaused}',
|
|
||||||
final run => l10n.pomodoroPhaseTitle(run),
|
|
||||||
},
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: [
|
|
||||||
if (run == null)
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: () => send(const PomodoroEventStartRequested()),
|
|
||||||
icon: const Icon(LucideIcons.play),
|
|
||||||
label: Text(l10n.pomodoroStart),
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
if (run.isRunning)
|
|
||||||
FilledButton.tonalIcon(
|
|
||||||
onPressed: () => send(const PomodoroEventPauseRequested()),
|
|
||||||
icon: const Icon(LucideIcons.pause),
|
|
||||||
label: Text(l10n.pomodoroPause),
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
FilledButton.icon(
|
Text(
|
||||||
onPressed: () => send(const PomodoroEventResumeRequested()),
|
l10n.pomodoroProfileName(profile),
|
||||||
icon: const Icon(LucideIcons.play),
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
label: Text(l10n.pomodoroResume),
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
IconButton(
|
||||||
onPressed: () => send(const PomodoroEventSkipRequested()),
|
tooltip: l10n.pomodoroProfiles,
|
||||||
icon: const Icon(LucideIcons.skipForward),
|
icon: const Icon(LucideIcons.settings2),
|
||||||
label: Text(l10n.pomodoroSkip),
|
onPressed: () => showPomodoroProfilesSheet(context),
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () => send(const PomodoroEventStopRequested()),
|
|
||||||
icon: const Icon(LucideIcons.square),
|
|
||||||
label: Text(l10n.pomodoroStop),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
const _TopBarSuggestion(),
|
Center(
|
||||||
],
|
child: PomodoroCountdown(
|
||||||
|
run: run ?? PomodoroRun.ready(profile),
|
||||||
|
style: theme.textTheme.displayMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
switch (run) {
|
||||||
|
null => l10n.pomodoroSummaryOf(profile),
|
||||||
|
final run when !run.isRunning =>
|
||||||
|
'${l10n.pomodoroPhaseTitle(run)} · ${l10n.pomodoroPaused}',
|
||||||
|
final run => l10n.pomodoroPhaseTitle(run),
|
||||||
|
},
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
if (run == null)
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => send(const PomodoroEventStartRequested()),
|
||||||
|
icon: const Icon(LucideIcons.play),
|
||||||
|
label: Text(l10n.pomodoroStart),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
if (run.isRunning)
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
onPressed: () =>
|
||||||
|
send(const PomodoroEventPauseRequested()),
|
||||||
|
icon: const Icon(LucideIcons.pause),
|
||||||
|
label: Text(l10n.pomodoroPause),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () =>
|
||||||
|
send(const PomodoroEventResumeRequested()),
|
||||||
|
icon: const Icon(LucideIcons.play),
|
||||||
|
label: Text(l10n.pomodoroResume),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => send(const PomodoroEventSkipRequested()),
|
||||||
|
icon: const Icon(LucideIcons.skipForward),
|
||||||
|
label: Text(l10n.pomodoroSkip),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => send(const PomodoroEventStopRequested()),
|
||||||
|
icon: const Icon(LucideIcons.square),
|
||||||
|
label: Text(l10n.pomodoroStop),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const _TopBarSuggestion(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/bloc/pomodoro_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/bloc/pomodoro_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_texts.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_texts.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/count_stepper.dart';
|
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/study_queue.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_options.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_options.dart';
|
||||||
|
|
||||||
/// Where the studying of some cards stands, as the library shows it, with
|
/// Where the studying of some cards stands, as the library shows it, with
|
||||||
/// how many notes and learning paths there are beside them.
|
/// how many notes, learning paths and files (PDFs) there are beside them.
|
||||||
final class const StudyProgress({
|
final class const StudyProgress({
|
||||||
/// How many cards there are.
|
/// How many cards there are.
|
||||||
required final int cards,
|
required final int cards,
|
||||||
|
|
@ -26,23 +26,39 @@ final class const StudyProgress({
|
||||||
/// How many learning paths there are with the cards, counted as notes
|
/// How many learning paths there are with the cards, counted as notes
|
||||||
/// are.
|
/// are.
|
||||||
final int paths = 0,
|
final int paths = 0,
|
||||||
|
|
||||||
|
/// How many files (PDFs) there are with the cards, counted as notes are.
|
||||||
|
final int files = 0,
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
/// The share of mature cards, from 0 to 1; null without cards.
|
/// The share of mature cards, from 0 to 1; null without cards.
|
||||||
double? get mastery => cards == 0 ? null : mature / cards;
|
double? get mastery => cards == 0 ? null : mature / cards;
|
||||||
|
|
||||||
/// This progress with [notes] notes and [paths] paths beside the cards.
|
/// This progress with [notes] notes, [paths] paths and [files] files
|
||||||
StudyProgress withContent({required int notes, required int paths}) =>
|
/// beside the cards.
|
||||||
StudyProgress(
|
StudyProgress withContent({
|
||||||
cards: cards,
|
required int notes,
|
||||||
due: due,
|
required int paths,
|
||||||
reviewsDue: reviewsDue,
|
required int files,
|
||||||
mature: mature,
|
}) => StudyProgress(
|
||||||
notes: notes,
|
cards: cards,
|
||||||
paths: paths,
|
due: due,
|
||||||
);
|
reviewsDue: reviewsDue,
|
||||||
|
mature: mature,
|
||||||
|
notes: notes,
|
||||||
|
paths: paths,
|
||||||
|
files: files,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [cards, due, reviewsDue, mature, notes, paths];
|
List<Object?> get props => [
|
||||||
|
cards,
|
||||||
|
due,
|
||||||
|
reviewsDue,
|
||||||
|
mature,
|
||||||
|
notes,
|
||||||
|
paths,
|
||||||
|
files,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The progress of the cards in [groups] (each studyset's, in tree order, as
|
/// The progress of the cards in [groups] (each studyset's, in tree order, as
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue