feat(learn,todo,train,meditate,ui): the navigation bar flush with the foot, a fade above it
Below medium windows the frame's bar is Material 3 Expressive's again, flush with the window's foot (decided 7 Nov 2026 by the developer): the theme's NavigationBar across the whole width, 64 dp in its own colour, running on beneath the system's bar as Material's does. Right above it a fade 32 dp high, from the background of the screen in view (the ScreenTint's, the theme's surface before a screen claims one) made see-through to that background, so what scrolls on beneath the bar fades out rather than being cut off; taps pass through it. The rail of wider windows stays as it was. halkyon_ui's FloatingNavigationBar becomes FlushNavigationBar, its side margin, lift and rounded card gone. Its coverOf is now the whole room content leaves at its end, the system's bar included (the body's bottom padding: bar, fade and system's bar inside the frame, the system's bar alone outside it), and floating buttons rise by buttonLiftOf, the same without the system's bar, which a Scaffold keeps them clear of itself. Lists, Todo's hour grid and the Plan's sheet keep using coverOf; the Profile's list no longer adds the system's bar a second time. Whats-New: The navigation bar sits at the bottom edge again, with a soft fade above it. Whats-New-De: Die Navigationsleiste sitzt wieder am unteren Rand, mit einem sanften Übergang darüber. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ccdf22b936
commit
05db5e2f45
47 changed files with 425 additions and 217 deletions
|
|
@ -8,7 +8,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
|||
|
||||
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`; the apps' icons are the root's `tool/app_icons.py`, ADR 0031). Learn is the app that runs; Train, Todo and Meditate run too (ADR 0030, ADR 0033, ADR 0038), Todo and Meditate not released yet.
|
||||
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
||||
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the floating bar and rail, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
|
||||
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the navigation bar flush with the foot with its fade, and the rail, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
|
||||
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyon_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
||||
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, the account section of an app's settings (`AccountTiles`), every app's Profile (`HalkyonProfile`, decided 2026-11-02: the person's own tab, the same in every app, with their account, the ways they sign in and their public profile, one for all apps, then the app's own tabs) and people's public profiles (`PublicProfileRepository`, `PublicProfileSection`, `showProfileOf`), the wait screen of an account waiting for approval and the operator's card, badge and screen (ADR 0021), the assistants of an app's settings (`AssistantsCard`: granted, by provider, agreed to, what is left of the person's budget, used, and the person's own keys) and the operator's grants from that screen (`AssistantGrantsScreen`: several assistants to several people at once, each with one budget, D57 to D63), with the repositories asking them through the module, at an effort the operator allows, or a person's own key directly (step 2) and what an app's warning before a private studyset goes names (`providerTermsOf`, `OwnAssistant.onThisMachine`, step 3), About with the instance's source link (`showAppAbout`), the device's own speech recognition (`SpeechInput`, `DeviceSpeechInput` on `speech_to_text`, `FakeSpeechInput` in `testing.dart`; none on Linux) and dictation into a text at its cursor, listening again after each pause (`Dictation`, Meditate's ADR 0038, D15; an app that listens declares the microphone and its usage texts itself), what's new after an update (`WhatsNew`, `WhatsNewOnUpdate`, `WhatsNewTile`, from the app's `assets/whats_new/`, Train's ADR 0030, D61), the device's appearance and language, chosen in Settings and applied before anyone signs in (`Look`, `FileLookStore`, `LookCubit`, `DeviceLookCard`; Learn keeps them among its own device settings and shows `LookSettingsCard`; ADR 0014's amendment of 4 Nov 2026), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
|
||||
- `halkyon_editor` (ADR 0033, D37, 6 Nov 2026): rich text, the general parts of Learn's editor on our fork of `appflowy_editor`: `RichContent` (the editor's document JSON, which Learn and Todo store), `RichContentEditor`, `RichContentView`, the format bar, links, the clipboard keeping formats, superscript and subscript, and `EditorBlocks`, through which an app adds blocks, marks within lines and their buttons (`basicBlocks` without any), with texts of its own (English, German). What is Learn's alone stays in Learn on top of it.
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
|||
- **UTC for storing and sending, local time for display and day boundaries.** Serverpod sends `DateTime` as UTC and stores it in `timestamp without time zone` columns. Compare and compute in UTC; call `.toLocal()` only to display. "Due today", streaks and daily goals use the learner's **local** calendar day, computed on the client; the server never needs day boundaries.
|
||||
- **Build configuration comes from `--dart-define`.** The client holds no secrets, ever.
|
||||
- **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`, halkyon_ui's `TabsStack`: go_router's indexed stack plus `TabVisibility`, which a screen depends on to stop something once its tab is hidden); `AppShell` shows them through halkyon_ui's `NavigationFrame`, every Halkyon app's (decided 2026-10-30), as a navigation rail from 600 dp on and as a navigation bar below (Material 3 Expressive's shorter one, 64 dp, set in the theme: Flutter's Material library has none yet), floating above the bottom edge (`FloatingNavigationBar`): the tab screens run on beneath it (the shell's `extendBody`), every list ending with room for it (the body's bottom padding) and floating buttons lifted by `FloatingNavigationBar.coverOf`, with the background of the screen in view around it in between (`ScreenTint`); it steps aside inside a studyset (its screen, notes, paths and dictionary: `RoutePaths.isInsideStudyset`), whose own bar at the bottom takes its place, leaving a strip as high as the system's bar (decided 2026-10-11, as Material 3 puts toolbars below the top level and Vaia does), 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 **the screens behind a studyset are its path** (decided 2026-10-11; `feature/studyset/widget/studyset_navigation.dart`): a topic, at any depth, opens with the screens of the levels between pushed beneath it (`openStudysetWithin`; the same route, keyed by id and named by its location, so several stack), the path's steps and the levels a long press on back lists go up by popping to that page (`goUpTo`), and a studyset opened from elsewhere gets its levels beneath it (`openStudyset`). So back, Android's predictive back, iOS's swipe from the edge and the path all lead one level up. Below expanded windows the topics open in a sheet from the end of the path's line, never a drawer, whose swipe from the edge is iOS's way back. 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 lists what is published (see *Explore and copies*).
|
||||
- **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`, halkyon_ui's `TabsStack`: go_router's indexed stack plus `TabVisibility`, which a screen depends on to stop something once its tab is hidden); `AppShell` shows them through halkyon_ui's `NavigationFrame`, every Halkyon app's (decided 2026-10-30), as a navigation rail from 600 dp on and as a navigation bar below (Material 3 Expressive's shorter one, 64 dp, set in the theme: Flutter's Material library has none yet), flush with the bottom edge (`FlushNavigationBar`, decided 7 Nov 2026 by the developer in place of a bar floating above it: Material 3 Expressive keeps the bar flush, 64 dp, and a fade replaces the floating card), with a fade 32 dp high above it into the background of the screen in view (`ScreenTint`), which taps pass through: the tab screens run on beneath both (the shell's `extendBody`), every list ending with room for them (the body's bottom padding, `FlushNavigationBar.coverOf`: the bar, its fade and the system's bar) and floating buttons lifted by `FlushNavigationBar.buttonLiftOf` (the same without the system's bar, which a Scaffold keeps its button clear of itself); it steps aside inside a studyset (its screen, notes, paths and dictionary: `RoutePaths.isInsideStudyset`), whose own bar at the bottom takes its place, leaving a strip as high as the system's bar (decided 2026-10-11, as Material 3 puts toolbars below the top level and Vaia does), 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 **the screens behind a studyset are its path** (decided 2026-10-11; `feature/studyset/widget/studyset_navigation.dart`): a topic, at any depth, opens with the screens of the levels between pushed beneath it (`openStudysetWithin`; the same route, keyed by id and named by its location, so several stack), the path's steps and the levels a long press on back lists go up by popping to that page (`goUpTo`), and a studyset opened from elsewhere gets its levels beneath it (`openStudyset`). So back, Android's predictive back, iOS's swipe from the edge and the path all lead one level up. Below expanded windows the topics open in a sheet from the end of the path's line, never a drawer, whose swipe from the edge is iOS's way back. 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 lists what is published (see *Explore and copies*).
|
||||
- **Routes carry ids, never objects.** A screen finds its item in the repository's live list with `FoundById`: 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, every Halkyon app's**: the theme, its fonts, the window sizes and the shared widgets come from `halkyon_ui` (`packages/halkyon_ui`, decided 2026-10-18). Change them there, not here. `appTheme(brightness, extensions:)` with hand-written light and dark colour schemes around the brand's cobalt and copper, which Learn calls as `learnTheme` (`core/theme/`) with its own colours beside; `Theme.of(context).extras` (`ThemeExtras`) for the states Material has no role for, and `Theme.of(context).learnColors` (`LearnColors`) for Learn's own (the four FSRS ratings, a starred star); `PaletteColor` for the 20 colours a person can give content; snack bars floating above the navigation bar as Material 3 places them, in its inverse colours (dark in the light theme), lined up with the floating bar, 560 wide and centred from medium windows on (`FittedSnackBars`, decided 2026-10-12; Flutter's default is Material 2's fixed bar across the foot); `TopGradient` for a colour washing down from the top of every tab's first screen and Settings (cobalt) and of every screen of a studyset (its colour, own or inherited, which `_inStudyset` in the router hands down as `ScreenColor`): the studyset, studying it, its cards, writing them, its notes, paths and dictionary; behind a clear app bar, in dark mode a deep shade (42 % of the colour over black), in light mode a tint, held to WCAG AA for the theme's text. From medium windows on, a `TintFrame` takes the colour of the screen in view (`ScreenTint`, claimed by the `TopGradient` whose tab shows and whose route is on top), fading out by 60 % of the height: the rail sits on it, transparent, and the screen is a rounded panel inset beside it, its own wash half as strong; screens over the tabs (studying, the card browser and composer, Settings) sit in the same frame without the rail (`FramedScreen`). As Vaia does, decided 2026-10-10. The PDF reader and the reading timeline stay plain. halkyon_ui's `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, and Learn's `learn_theme_test.dart` a starred star to 3:1 on the cards. **Fonts are bundled, never fetched** (halkyon_ui's `app_fonts.dart`; the CJK fonts are the app's own, `assets/fonts/ATTRIBUTION.md`, their licence registered by `core/theme/cjk_fonts.dart`, under the families halkyon_ui's fallback names): Noto Sans, from halkyon_ui, is the app's font on every device (Latin, Greek with polytonic, Cyrillic); Noto Sans JP, SC, TC and KR fill in Japanese, Chinese and Korean, Japanese first unless a studyset's language puts its own first (`StudysetLanguageScope`, which every studyset route wraps its screen in through `_inStudyset`, and the library's and Explore's cards each around their studyset; Han characters take the first listed font's shapes); each is one variable font holding every weight, and their OFL licences join the licence list. Scripts none covers (Arabic, Hebrew, Thai, …) fall back to the device's own fonts. A studyset's languages (`frontLanguage`, `backLanguage`, BCP 47) are given together or inherited together from the nearest studyset above (`languagesOf`). Never add `google_fonts` or anything else that downloads fonts. Layouts follow Material 3's window-size classes (`context.windowSize`), 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 come from halkyon_ui (`SectionCard`, `AppLoadingIndicator` (every spinner is Material 3 Expressive's, in buttons `ButtonLoadingIndicator`, pulling a list down `AppRefreshIndicator`, never Flutter's older circular one), `confirmDestructiveAction`, `FoundById`, `TitleWithKicker`, `showUndoSnackBar`, `ScrollHidingFab`, `PaletteColorPicker`, `CountStepper`, `requiredDecoration` (every required field: while empty its outline stronger, in the primary colour, and "Required" below it; never an asterisk) and `FieldInfoButton`, `ReorderToggle`, `ReorderGrip`, `CollapsingRow`, `CountBadge`, `liftWhileDragged`, `LaunchScreen` and `BrandWordmark` ("Halkyon" over an app's word), `SliverCentered` and `EmptyListMessage` (what an empty list shows: icon, "No … yet", and which menus add some), `Shimmer` and `SkeletonBox` (what a list shows while it loads in place of a spinner, the Library's cards since 1 Nov 2026), and the sheets below), with texts of their own (`HalkyonUiLocalizations`); Learn's own are in `core/widget/` (`ConflictVersions`, the versions of a conflict each editor shows, with Keep, its save status beside it `SaveStatusLabel` in `core/editor/`; `ContentCountsLine`; `AppLaunchScreen`, what shows while the app opens: the kingfisher, "Halkyon" over "Learn", whose wordmark the first start shows too).
|
||||
- **The look is one theme, every Halkyon app's**: the theme, its fonts, the window sizes and the shared widgets come from `halkyon_ui` (`packages/halkyon_ui`, decided 2026-10-18). Change them there, not here. `appTheme(brightness, extensions:)` with hand-written light and dark colour schemes around the brand's cobalt and copper, which Learn calls as `learnTheme` (`core/theme/`) with its own colours beside; `Theme.of(context).extras` (`ThemeExtras`) for the states Material has no role for, and `Theme.of(context).learnColors` (`LearnColors`) for Learn's own (the four FSRS ratings, a starred star); `PaletteColor` for the 20 colours a person can give content; snack bars floating above the navigation bar as Material 3 places them, in its inverse colours (dark in the light theme), 16 in from the sides and 8 above the bar's fade, 560 wide and centred from medium windows on (`FittedSnackBars`, decided 2026-10-12; Flutter's default is Material 2's fixed bar across the foot); `TopGradient` for a colour washing down from the top of every tab's first screen and Settings (cobalt) and of every screen of a studyset (its colour, own or inherited, which `_inStudyset` in the router hands down as `ScreenColor`): the studyset, studying it, its cards, writing them, its notes, paths and dictionary; behind a clear app bar, in dark mode a deep shade (42 % of the colour over black), in light mode a tint, held to WCAG AA for the theme's text. From medium windows on, a `TintFrame` takes the colour of the screen in view (`ScreenTint`, claimed by the `TopGradient` whose tab shows and whose route is on top), fading out by 60 % of the height: the rail sits on it, transparent, and the screen is a rounded panel inset beside it, its own wash half as strong; screens over the tabs (studying, the card browser and composer, Settings) sit in the same frame without the rail (`FramedScreen`). As Vaia does, decided 2026-10-10. The PDF reader and the reading timeline stay plain. halkyon_ui's `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, and Learn's `learn_theme_test.dart` a starred star to 3:1 on the cards. **Fonts are bundled, never fetched** (halkyon_ui's `app_fonts.dart`; the CJK fonts are the app's own, `assets/fonts/ATTRIBUTION.md`, their licence registered by `core/theme/cjk_fonts.dart`, under the families halkyon_ui's fallback names): Noto Sans, from halkyon_ui, is the app's font on every device (Latin, Greek with polytonic, Cyrillic); Noto Sans JP, SC, TC and KR fill in Japanese, Chinese and Korean, Japanese first unless a studyset's language puts its own first (`StudysetLanguageScope`, which every studyset route wraps its screen in through `_inStudyset`, and the library's and Explore's cards each around their studyset; Han characters take the first listed font's shapes); each is one variable font holding every weight, and their OFL licences join the licence list. Scripts none covers (Arabic, Hebrew, Thai, …) fall back to the device's own fonts. A studyset's languages (`frontLanguage`, `backLanguage`, BCP 47) are given together or inherited together from the nearest studyset above (`languagesOf`). Never add `google_fonts` or anything else that downloads fonts. Layouts follow Material 3's window-size classes (`context.windowSize`), 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 come from halkyon_ui (`SectionCard`, `AppLoadingIndicator` (every spinner is Material 3 Expressive's, in buttons `ButtonLoadingIndicator`, pulling a list down `AppRefreshIndicator`, never Flutter's older circular one), `confirmDestructiveAction`, `FoundById`, `TitleWithKicker`, `showUndoSnackBar`, `ScrollHidingFab`, `PaletteColorPicker`, `CountStepper`, `requiredDecoration` (every required field: while empty its outline stronger, in the primary colour, and "Required" below it; never an asterisk) and `FieldInfoButton`, `ReorderToggle`, `ReorderGrip`, `CollapsingRow`, `CountBadge`, `liftWhileDragged`, `LaunchScreen` and `BrandWordmark` ("Halkyon" over an app's word), `SliverCentered` and `EmptyListMessage` (what an empty list shows: icon, "No … yet", and which menus add some), `Shimmer` and `SkeletonBox` (what a list shows while it loads in place of a spinner, the Library's cards since 1 Nov 2026), and the sheets below), with texts of their own (`HalkyonUiLocalizations`); Learn's own are in `core/widget/` (`ConflictVersions`, the versions of a conflict each editor shows, with Keep, its save status beside it `SaveStatusLabel` in `core/editor/`; `ContentCountsLine`; `AppLaunchScreen`, what shows while the app opens: the kingfisher, "Halkyon" over "Learn", whose wordmark the first start shows too).
|
||||
- **A studyset's options are the same wherever a menu of it opens** (`studysetOptions` and `doStudysetOption` in `studyset_actions.dart`, decided 2026-10-12): its card in the library, its own screen, a topic beside it. Import content runs where the studyset's blocs are, on its screen: from elsewhere it opens the screen, which asks what to import as it opens (`?import=content`).
|
||||
- **A studyset's content is in three parts** (`feature/studyset/widget/studyset_parts.dart`, decided 2026-10-12, as Vaia has it): Material (its learning paths, flashcards, notes and files in tabs, the paths first as the way through the rest (decided 2026-10-13: a path is material too, which the plan refers to), each saying how many it holds, an empty one nothing so the four fit a phone more often, each list whole with its tools at the end of the tabs' row, which fold one by one where room runs short rather than cutting the tabs off (`CollapsingRow`: Browse's words first, then the filter into the ⋮ menu, then Browse; as the studyset's own actions fold into icons); decided 2026-10-12 after a trial of sections one below the other, the first three of each behind Show all, as Vaia stacks them: the lists run to hundreds of cards, and tabs are Material 3's way within a part; the tab is kept as the studyset's part, while the material shows, and opened for the first time it opens on the first tab with something in it; a tab with none of its own while the studyset's topics hold some says that it is in them, each topic holding some with how much, everything inside it counted, opening it, Browse all for flashcards, and how to add some here after all, decided 2026-10-14 after people testing the app looked for a grouping studyset's material in its empty tabs; the tabs' counts stay its own), Progress (always, nothing yet where nothing was studied, so the switch never changes as reviews are read, decided 2026-10-12: how much is learnt, what is due today, the study time this week from the reviews' durations, each at most `Review.maxStudyTime`, `StudysetActivityCubit`; how many steps of its learning paths are done and the PDFs not read to their end, begun first, each opening the reader, decided 2026-10-17: `MaterialProgressCubit`; all of it across its topics) and Study plan (the paths finished; Next up: studying for the exam while one is ahead and its pace leaves cards for the day, then the cards due, then the next step of each path under way, or the first path not finished when none is; how far each path has come, its steps done in a bar, each opening the path; managing the paths is the material's; at the top the exam the person prepares for, below; `PathListState.stepsDone`). `StudysetPartSwitch` chooses between them in the header: below expanded windows in a line of its own under the title, from there on in the path's line, which the topics leave free there, centred above the material's column (at the line's end where that column is narrower than the switch; the path folds before it); it ticks as the navigation bar does. **A studyset opens on the part it was left on, on this device** (`StudysetTabs`, `studyset_tabs.json` in the person's folder, read as their data opens, decided 2026-10-11; a tab kept before the parts counts as its part); opened there for the first time, on the study plan while a path is unfinished, else on the material, showing that it loads until it knows rather than one part and then another. Material added or imported shows in its part (`ContentTabCubit`).
|
||||
- **Statistics** (`feature/statistics/`, decided 2026-10-12, as Vaia has them): the Profile tab's second tab (the tab row folding into the title as the content scrolls, halkyon_ui's `FoldingTabs`, as Strava's). The time studied in all, per day (every day from the first studied to today counted) and the most in one day, and by days, weeks or months (ten, ten, twelve bars) the study time and the ratings, as bars the app draws itself (`BarChart`, no chart package); a tap on a bar says what it holds. Computed on the device from every review (`studyStatistics`, `StatisticsCubit`), every rating counted, crammed ones too, a fresh start not, and **a review counts as at most a minute of study** (`Review.maxStudyTime`, as Anki does by default: a card left on the screen over lunch isn't an hour of study; stored durations stay as they were), here and in a studyset's Progress. What waits in Recently deleted counts nowhere, here or on Home.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
/// The frame around the four tabs, every Halkyon app's ([NavigationFrame]):
|
||||
/// a navigation rail at the side from medium windows on, the app's icon atop
|
||||
/// it, and a navigation bar floating above the bottom below that. Every tab
|
||||
/// it, and a navigation bar flush with the bottom below that. Every tab
|
||||
/// keeps its own stack of screens, and the navigation stays visible on all
|
||||
/// of them; screens that need the whole window are routes outside the shell.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -502,7 +502,7 @@ class const _Results({required final ExploreState state})
|
|||
),
|
||||
),
|
||||
),
|
||||
// Room at the end for the navigation bar floating over the foot.
|
||||
// Room at the end for the navigation bar and its fade at the foot.
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 12 + MediaQuery.paddingOf(context).bottom,
|
||||
|
|
|
|||
|
|
@ -110,13 +110,13 @@ class _HomeState() extends State<_Home> {
|
|||
// for the navigation bar.
|
||||
extendBody: true,
|
||||
// Folded into its button at the lower left, as in the Library (decided
|
||||
// 2026-11-01), clear of the navigation bar floating over the foot.
|
||||
// 2026-11-01), clear of the navigation bar and its fade at the foot.
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
|
||||
floatingActionButton: _arranging || !assistantOn(context)
|
||||
? null
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: FloatingNavigationBar.coverOf(context),
|
||||
bottom: FlushNavigationBar.buttonLiftOf(context),
|
||||
),
|
||||
child: const AssistantButton(topic: AssistantTopicLearning()),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class _StatisticsState() extends State<_Statistics> {
|
|||
constraints: const BoxConstraints(maxWidth: StatisticsView.maxWidth),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => ListView(
|
||||
// Room at the end for the navigation bar floating over the foot.
|
||||
// Room at the end for the navigation bar and its fade at the foot.
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
|
|
|
|||
|
|
@ -266,12 +266,12 @@ class _LibraryState() extends State<_Library> {
|
|||
(true, null) => FloatingActionButtonLocation.startFloat,
|
||||
(false, _) => FloatingActionButtonLocation.endFloat,
|
||||
},
|
||||
// Clear of the navigation bar floating over the foot on a phone.
|
||||
// Clear of the navigation bar and its fade at the foot on a phone.
|
||||
floatingActionButton: fab == null
|
||||
? null
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: FloatingNavigationBar.coverOf(context),
|
||||
bottom: FlushNavigationBar.buttonLiftOf(context),
|
||||
),
|
||||
child: fab,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -118,40 +118,43 @@ void main() {
|
|||
expect(find.byType(NavigationRail), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the navigation bar floats 16 in from the sides and 8 above '
|
||||
"the system's bar, the background of the screen in view around it", (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('the navigation bar is flush with the foot, as wide as the '
|
||||
'window; above it the screens fade into the background of the one in '
|
||||
'view', (tester) async {
|
||||
tester.view.padding = const FakeViewPadding(bottom: 24);
|
||||
tester.view.viewPadding = const FakeViewPadding(bottom: 24);
|
||||
addTearDown(tester.view.resetPadding);
|
||||
addTearDown(tester.view.resetViewPadding);
|
||||
await showApp(tester, width: 400);
|
||||
final bar = find.byType(FloatingNavigationBar);
|
||||
Rect pill() => tester.getRect(
|
||||
find.descendant(of: bar, matching: find.byType(Material)).first,
|
||||
);
|
||||
Color? around() => tester
|
||||
.widget<Scaffold>(
|
||||
find.ancestor(of: bar, matching: find.byType(Scaffold)),
|
||||
)
|
||||
.backgroundColor;
|
||||
final bar = find.byType(NavigationBar);
|
||||
final fade = find.byKey(FlushNavigationBar.fadeKey);
|
||||
Color fadesInto() =>
|
||||
((tester.widget<DecoratedBox>(fade).decoration as BoxDecoration)
|
||||
.gradient!
|
||||
as LinearGradient)
|
||||
.colors
|
||||
.last;
|
||||
final scheme = Theme.of(tester.element(bar)).colorScheme;
|
||||
|
||||
expect(
|
||||
pill(),
|
||||
const Rect.fromLTRB(16, 900 - 24 - 8 - 64, 400 - 16, 900 - 24 - 8),
|
||||
tester.getRect(bar),
|
||||
const Rect.fromLTRB(0, 900 - 24 - 64, 400, 900),
|
||||
);
|
||||
expect(around(), scheme.surface);
|
||||
expect(tester.getRect(fade).bottom, 900 - 24 - 64);
|
||||
expect(fadesInto(), scheme.surface);
|
||||
|
||||
await openTab(tester, 'Library');
|
||||
expect(around(), scheme.surfaceContainerLow);
|
||||
expect(fadesInto(), scheme.surfaceContainerLow);
|
||||
});
|
||||
|
||||
testWidgets('a snack bar floats 8 above the navigation bar, as far in '
|
||||
'from the sides; from medium windows on 560 wide, centred', (
|
||||
testWidgets('a snack bar floats 8 above the navigation bar and its fade, '
|
||||
'16 in from the sides; from medium windows on 560 wide, centred', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.padding = const FakeViewPadding(bottom: 24);
|
||||
tester.view.viewPadding = const FakeViewPadding(bottom: 24);
|
||||
addTearDown(tester.view.resetPadding);
|
||||
addTearDown(tester.view.resetViewPadding);
|
||||
Future<Rect> snackBar() async {
|
||||
await openTab(tester, 'Library');
|
||||
ScaffoldMessenger.of(
|
||||
|
|
@ -170,8 +173,11 @@ void main() {
|
|||
final narrow = await snackBar();
|
||||
expect(narrow.left, 16);
|
||||
expect(narrow.right, 400 - 16);
|
||||
// The bar's top, less 8.
|
||||
expect(narrow.bottom, 900 - 24 - 8 - 64 - 8);
|
||||
// The fade's top, less 8.
|
||||
expect(
|
||||
narrow.bottom,
|
||||
900 - 24 - 64 - FlushNavigationBar.fadeHeight - 8,
|
||||
);
|
||||
|
||||
await showApp(tester);
|
||||
final wide = await snackBar();
|
||||
|
|
@ -183,7 +189,9 @@ void main() {
|
|||
'studyset and its notes, and comes back in the library; inside, the '
|
||||
"screens keep clear of the system's bar", (tester) async {
|
||||
tester.view.padding = const FakeViewPadding(bottom: 24);
|
||||
tester.view.viewPadding = const FakeViewPadding(bottom: 24);
|
||||
addTearDown(tester.view.resetPadding);
|
||||
addTearDown(tester.view.resetViewPadding);
|
||||
await showApp(tester, width: 400);
|
||||
await openJapanese(tester);
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
|
|
@ -227,8 +235,8 @@ void main() {
|
|||
expect(find.byType(NavigationRail), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('on a phone, the tab screens run on beneath the floating bar, '
|
||||
'and their lists end with room to scroll out from under it', (
|
||||
testWidgets('on a phone, the tab screens run on beneath the bar and its '
|
||||
'fade, and their lists end with room to scroll out from under both', (
|
||||
tester,
|
||||
) async {
|
||||
for (var i = 0; i < 12; i++) {
|
||||
|
|
@ -239,18 +247,14 @@ void main() {
|
|||
);
|
||||
}
|
||||
tester.view.padding = const FakeViewPadding(bottom: 24);
|
||||
tester.view.viewPadding = const FakeViewPadding(bottom: 24);
|
||||
addTearDown(tester.view.resetPadding);
|
||||
addTearDown(tester.view.resetViewPadding);
|
||||
await showApp(tester, width: 400);
|
||||
await openTab(tester, 'Library');
|
||||
|
||||
final pill = tester.getRect(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(FloatingNavigationBar),
|
||||
matching: find.byType(Material),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
// The fade's top: what ends above it is wholly in view.
|
||||
final pill = tester.getRect(find.byKey(FlushNavigationBar.fadeKey));
|
||||
expect(tester.getBottomLeft(find.byType(LibraryScreen)).dy, 900);
|
||||
// Before scrolling, which slides it away.
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class const CourseScreen({required final String courseId, super.key})
|
|||
16,
|
||||
16,
|
||||
16,
|
||||
16 + FloatingNavigationBar.coverOf(context),
|
||||
16 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class _LibraryItemScreenState() extends State<LibraryItemScreen> {
|
|||
16,
|
||||
16,
|
||||
16,
|
||||
16 + FloatingNavigationBar.coverOf(context),
|
||||
16 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -69,13 +69,13 @@ class _PracticeScreenState() extends State<PracticeScreen> {
|
|||
maxWidth: readableContentMaxWidth,
|
||||
),
|
||||
child: ListView(
|
||||
// Room at the end for the navigation bar floating over
|
||||
// Room at the end for the navigation bar and its fade at
|
||||
// the foot: the page is in Explore's tab.
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16 + FloatingNavigationBar.coverOf(context),
|
||||
16 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
/// The frame around Meditate's tabs (ADR 0038, D1, D2, D14): halkyon_ui's
|
||||
/// [NavigationFrame], as every Halkyon app's, with Today (where it opens),
|
||||
/// Explore, the Journal and Profile, a bar floating above the foot below medium windows
|
||||
/// Explore, the Journal and Profile, a bar flush with the foot below medium windows
|
||||
/// and a rail with Meditate's icon on top from medium on. Every tab keeps
|
||||
/// its own stack of screens. After an update, what's new shows once
|
||||
/// (halkyon_app's [WhatsNewOnUpdate]), where the app is given it.
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ class const TabScreen({
|
|||
actions: [...actions, const SizedBox(width: 4)],
|
||||
),
|
||||
body: body,
|
||||
// Clear of the navigation bar floating over the foot on a phone.
|
||||
// Clear of the navigation bar and its fade at the foot on a phone.
|
||||
floatingActionButton: switch (floatingActionButton) {
|
||||
final button? => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: FloatingNavigationBar.coverOf(context),
|
||||
bottom: FlushNavigationBar.buttonLiftOf(context),
|
||||
),
|
||||
child: button,
|
||||
),
|
||||
|
|
@ -46,7 +46,7 @@ class const TabScreen({
|
|||
}
|
||||
|
||||
/// A tab's cards, one under the other, as wide as readable text, with
|
||||
/// room at the end for the navigation bar floating over the foot.
|
||||
/// room at the end for the navigation bar and its fade at the foot.
|
||||
class const CardColumn({
|
||||
required final List<Widget> children,
|
||||
super.key,
|
||||
|
|
@ -61,7 +61,7 @@ class const CardColumn({
|
|||
12,
|
||||
8,
|
||||
12,
|
||||
12 + FloatingNavigationBar.coverOf(context),
|
||||
12 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
for (final (index, child) in children.indexed) ...[
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class const _TaskList({
|
|||
12,
|
||||
8,
|
||||
12,
|
||||
96 + FloatingNavigationBar.coverOf(context),
|
||||
96 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
if (description case final description? when !description.isBlank)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class const FiltersScreen({super.key}) extends StatelessWidget {
|
|||
12,
|
||||
8,
|
||||
12,
|
||||
16 + FloatingNavigationBar.coverOf(context),
|
||||
16 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
SectionCard(
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class _MonthGridState() extends State<MonthGrid> {
|
|||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 88 + FloatingNavigationBar.coverOf(context),
|
||||
bottom: 88 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ class _ProjectBoardState() extends State<ProjectBoard> {
|
|||
key: ProjectBoard.dotsKey,
|
||||
padding: EdgeInsets.only(
|
||||
top: 8,
|
||||
bottom: 8 + FloatingNavigationBar.coverOf(context),
|
||||
bottom: 8 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class const ProjectTable({
|
|||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 88 + FloatingNavigationBar.coverOf(context),
|
||||
bottom: 88 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ class const ProjectTasks({required final String projectId, super.key})
|
|||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 88 + FloatingNavigationBar.coverOf(context),
|
||||
bottom: 88 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -374,7 +374,7 @@ class const ProjectTasks({required final String projectId, super.key})
|
|||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 88 + FloatingNavigationBar.coverOf(context),
|
||||
bottom: 88 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class _ReportingScreenState() extends State<ReportingScreen> {
|
|||
12,
|
||||
8,
|
||||
12,
|
||||
24 + FloatingNavigationBar.coverOf(context),
|
||||
24 + FlushNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
SegmentedButton<_Page>(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue