feat(meditate): Settings choose the app's appearance and language
The developer asked for Meditate to offer what Learn's Settings offer first
(ADR 0014's amendment of 4 Nov 2026). Meditate passed a fixed look, the
system's theme and language; it now opens halkyon_app's LookCubit on
<app support>/look.json in main, provides it above InstanceGate, and its
start screens and each person's MaterialApp follow it. Settings show
DeviceLookCard right after the Sync and operator's cards, as Learn's,
offering English (UK), English (US) and Deutsch (meditateLanguages).
Today's streak line ("Noch keine Tage in Folge") overflowed a phone's
width in German in the tests' wide font, as it would with a large font
on a device; it wraps now (Flexible). The new test, choosing Deutsch and
starting again, found it.
Whats-New: Settings: choose the app's appearance and language.
Whats-New-De: Einstellungen: Darstellung und Sprache der App wählen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7aba2a2dcc
commit
bf9818e22d
7 changed files with 147 additions and 67 deletions
|
|
@ -4,7 +4,7 @@ What holds for every app and package of this repository is in the [root AGENTS.m
|
||||||
|
|
||||||
- **Packages**: `halkyon_meditate_server`, `halkyon_meditate_client` (generated, never edited), `halkyon_meditate_flutter`; app ID `app.halkyon.meditate` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`). Copied from Todo's tree on 3 Nov 2026 and stripped to the shell, so its frame, keys and sealed rows are Todo's and Train's.
|
- **Packages**: `halkyon_meditate_server`, `halkyon_meditate_client` (generated, never edited), `halkyon_meditate_flutter`; app ID `app.halkyon.meditate` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`). Copied from Todo's tree on 3 Nov 2026 and stripped to the shell, so its frame, keys and sealed rows are Todo's and Train's.
|
||||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`meditateBase()`, `lib/src/meditate_base.dart`): its basics say it serves `meditate` (never change it once apps use it), sign-in mails name Halkyon Meditate, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteMeditateData]`, `lib/src/meditate_data.dart`). **Accounts** (ADR 0016): Meditate holds its own register when it runs alone, and is a member of the instance's register where the operator sets `ACCOUNT_REGISTER_URL` (`AccountsMode.fromEnvironment()`) with the instance's shared `INSTANCE_SECRET`; `INSTANCE_MEMBER_MEDITATE` names it to the register. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled once each. Synced tables (ADR 0038, D7; `database: sync`, every row sealed): `content_key`, `meditate_settings` (one row a person, under an ID keyed by the account), `sitting`, `dream` (D14) and `lesson_done` (D16), each indexing `spaceId`. Migrations: `initial` (Serverpod's, the auth modules', the module's and the sync engine's tables), `sittings` (the three tables), `dreams` (`dream`) and `library` (`lesson_done`). Tests: `test/integration/meditate_server_test.dart` (the module's endpoints through the server's own dispatch, `halkyon_base.<endpoint>`, and the sync engine's connector refusing calls without a session), `test/integration/sitting/person_deletion_test.dart`, the library's in `test/unit/library/` (the manifest, the files over HTTP) and `test/integration/library/` (the endpoint).
|
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`meditateBase()`, `lib/src/meditate_base.dart`): its basics say it serves `meditate` (never change it once apps use it), sign-in mails name Halkyon Meditate, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteMeditateData]`, `lib/src/meditate_data.dart`). **Accounts** (ADR 0016): Meditate holds its own register when it runs alone, and is a member of the instance's register where the operator sets `ACCOUNT_REGISTER_URL` (`AccountsMode.fromEnvironment()`) with the instance's shared `INSTANCE_SECRET`; `INSTANCE_MEMBER_MEDITATE` names it to the register. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled once each. Synced tables (ADR 0038, D7; `database: sync`, every row sealed): `content_key`, `meditate_settings` (one row a person, under an ID keyed by the account), `sitting`, `dream` (D14) and `lesson_done` (D16), each indexing `spaceId`. Migrations: `initial` (Serverpod's, the auth modules', the module's and the sync engine's tables), `sittings` (the three tables), `dreams` (`dream`) and `library` (`lesson_done`). Tests: `test/integration/meditate_server_test.dart` (the module's endpoints through the server's own dispatch, `halkyon_base.<endpoint>`, and the sync engine's connector refusing calls without a session), `test/integration/sitting/person_deletion_test.dart`, the library's in `test/unit/library/` (the manifest, the files over HTTP) and `test/integration/library/` (the endpoint).
|
||||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Meditate's frame (`meditateFrame`) and `ServerpodInstanceChecker(app: 'meditate')`; `MeditateDependencies` per instance, `MeditatePerson` per person (their sync, `SettingsRepository`, `SittingRepository`, `DreamRepository`, `LibraryRepository`, `LessonRepository`, and the device's `BellPlayer`, `GuidePlayer` and `ScreenAwake`); SimpleLogin's way back on `app.halkyon.meditate://` and the loopback port 43826. The person's app (`MeditatePersonApp`) washes in fern (`ScreenColor`, D3) and holds `PracticeCubit` (settings and sittings, read again on every change), `JournalCubit` (the dreams) and `LibraryCubit` (the instance's library, lessons done, recordings kept) for every screen. go_router (`buildMeditateRouter`, `MeditatePaths`): halkyon_ui's `NavigationFrame` (`MeditateShell`) with Today, Explore, Journal and Profile (D2, D14; a bar below medium windows, a rail with the icon from medium on), a practice's page in Explore's branch (`PracticeScreen`) with a library item's and course's (`/explore/library/:id`, `?course=` for a lesson, `/explore/courses/:id`), and over the tabs the sitting (`SittingScreen`, handed its `SittingPlan` as `extra`), a recording playing (`ListeningScreen`, its `ListeningPlan` as `extra`), the library's credits (`/library-credits`), a dream (`DreamScreen`, `/dream` new, `/dream/:id` kept) and Settings. **Today** (`TodayTab`, D9): greeting by the hour, from 4 to 12 o'clock a card to write down last night's dream while it has none (`dreamPromptDue`, D14), Sit now (5, 10, 20 and the timer's own length), the practice of the day (`practiceOfDay`), the week and the days in a row (`WeekCard`), favourites, recently sat. **Explore** (`ExploreTab`, D10): search folded without case or accents (`matchesSearch`), four goal tiles, kind chips, practice cards. **The Journal** (`JournalTab`, D14; `lib/src/feature/journal/`): dreams by month, the newest first (`dreamsByMonth`), searched over title, text and labels and filtered by marks (`findDreams`), *+* to write one; a dream's night is the date of the morning after it (`defaultNight`: today's date at any hour); its screen writes with the text first and focused, the rest folded, and reads a kept one with Edit and Delete; a microphone under the text dictates where the device recognises speech (D15: halkyon_app's `Dictation` on `MeditatePerson.speech`, `DeviceSpeechInput` preferring Android's on-device recogniser, listening again after each pause until stopped or three silent listens; none on Linux, `NoSpeechInput` in tests that don't say), and Today's card offers *Speak it* (`/dream?speak=1`). The platforms declare the microphone as Learn's do (Android's `RECORD_AUDIO` and recognition query, iOS's and macOS's usage texts, macOS's `audio-input`), and the privacy page's points are `meditate_base.dart`'s. **Profile** (D2, D8): halkyon_app's `HalkyonProfile` with a *Practice* tab (minutes, sittings, days, the week, the journal's dreams this month and lucid ones once it holds one, every sitting with Delete). **The practices** are `practices` in `lib/src/feature/practice/model/practice.dart` (IDs never change once released), their words in the ARBs by ID (`practiceTexts`), their cues spread evenly over the length (`Practice.cueAt`). **A sitting** (`SittingCubit`, D6): its time from `clock.now()`, the ticks only showing it; the bell at the start (where on), between every interval and at the end; the screen kept on while it runs (where on); kept whole at its end, and ended early kept from a minute on where the person says so. **The figures** (`practiceFigures`, D8) on the device: a sitting's day by the wall clock it was sat by (`Sitting.offsetMinutes`), the streak up to today or yesterday, the week from the language's first day. **Settings** (D11): Sync, the operator's card, *Sitting* (bell, starting bell, bells between, screen on, the timer's length), *Practice* (the streak shown), *Journal* (Today's morning card, `dreamPrompt`), the account, What's new, About, the version. Texts in British English (the template) and German (du), `en_US` and `en_GB` empty. Widget tests start the whole app with halkyon_app's fakes (`test/app/meditate_app_test.dart`), the bells and screen recorded (`test/support/recording_device.dart`); a sitting on screen ticks every second, so tests pump seconds (`sit`) rather than `pumpAndSettle`; a test needing the hour (Today's morning card) starts the app inside `withClock` (`startAt`).
|
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Meditate's frame (`meditateFrame`) and `ServerpodInstanceChecker(app: 'meditate')`; `MeditateDependencies` per instance, `MeditatePerson` per person (their sync, `SettingsRepository`, `SittingRepository`, `DreamRepository`, `LibraryRepository`, `LessonRepository`, and the device's `BellPlayer`, `GuidePlayer` and `ScreenAwake`); SimpleLogin's way back on `app.halkyon.meditate://` and the loopback port 43826. The person's app (`MeditatePersonApp`) washes in fern (`ScreenColor`, D3) and holds `PracticeCubit` (settings and sittings, read again on every change), `JournalCubit` (the dreams) and `LibraryCubit` (the instance's library, lessons done, recordings kept) for every screen. go_router (`buildMeditateRouter`, `MeditatePaths`): halkyon_ui's `NavigationFrame` (`MeditateShell`) with Today, Explore, Journal and Profile (D2, D14; a bar below medium windows, a rail with the icon from medium on), a practice's page in Explore's branch (`PracticeScreen`) with a library item's and course's (`/explore/library/:id`, `?course=` for a lesson, `/explore/courses/:id`), and over the tabs the sitting (`SittingScreen`, handed its `SittingPlan` as `extra`), a recording playing (`ListeningScreen`, its `ListeningPlan` as `extra`), the library's credits (`/library-credits`), a dream (`DreamScreen`, `/dream` new, `/dream/:id` kept) and Settings. **Today** (`TodayTab`, D9): greeting by the hour, from 4 to 12 o'clock a card to write down last night's dream while it has none (`dreamPromptDue`, D14), Sit now (5, 10, 20 and the timer's own length), the practice of the day (`practiceOfDay`), the week and the days in a row (`WeekCard`), favourites, recently sat. **Explore** (`ExploreTab`, D10): search folded without case or accents (`matchesSearch`), four goal tiles, kind chips, practice cards. **The Journal** (`JournalTab`, D14; `lib/src/feature/journal/`): dreams by month, the newest first (`dreamsByMonth`), searched over title, text and labels and filtered by marks (`findDreams`), *+* to write one; a dream's night is the date of the morning after it (`defaultNight`: today's date at any hour); its screen writes with the text first and focused, the rest folded, and reads a kept one with Edit and Delete; a microphone under the text dictates where the device recognises speech (D15: halkyon_app's `Dictation` on `MeditatePerson.speech`, `DeviceSpeechInput` preferring Android's on-device recogniser, listening again after each pause until stopped or three silent listens; none on Linux, `NoSpeechInput` in tests that don't say), and Today's card offers *Speak it* (`/dream?speak=1`). The platforms declare the microphone as Learn's do (Android's `RECORD_AUDIO` and recognition query, iOS's and macOS's usage texts, macOS's `audio-input`), and the privacy page's points are `meditate_base.dart`'s. **Profile** (D2, D8): halkyon_app's `HalkyonProfile` with a *Practice* tab (minutes, sittings, days, the week, the journal's dreams this month and lucid ones once it holds one, every sitting with Delete). **The practices** are `practices` in `lib/src/feature/practice/model/practice.dart` (IDs never change once released), their words in the ARBs by ID (`practiceTexts`), their cues spread evenly over the length (`Practice.cueAt`). **A sitting** (`SittingCubit`, D6): its time from `clock.now()`, the ticks only showing it; the bell at the start (where on), between every interval and at the end; the screen kept on while it runs (where on); kept whole at its end, and ended early kept from a minute on where the person says so. **The figures** (`practiceFigures`, D8) on the device: a sitting's day by the wall clock it was sat by (`Sitting.offsetMinutes`), the streak up to today or yesterday, the week from the language's first day. **Settings** (D11): Sync, the operator's card, the device's appearance and language (halkyon_app's `DeviceLookCard` on `LookCubit`, kept in `<app support>/look.json`, `meditateLanguages`; ADR 0014's amendment of 4 Nov 2026), *Sitting* (bell, starting bell, bells between, screen on, the timer's length), *Practice* (the streak shown), *Journal* (Today's morning card, `dreamPrompt`), the account, What's new, About, the version. Texts in British English (the template) and German (du), `en_US` and `en_GB` empty. Widget tests start the whole app with halkyon_app's fakes (`test/app/meditate_app_test.dart`), the bells and screen recorded (`test/support/recording_device.dart`); a sitting on screen ticks every second, so tests pump seconds (`sit`) rather than `pumpAndSettle`; a test needing the hour (Today's morning card) starts the app inside `withClock` (`startAt`).
|
||||||
- **The operator's library** (D16; `lib/src/library/` on the server, `lib/src/feature/library/` in the app): a folder named by `MEDITATE_LIBRARY_DIR` with recordings, pictures and `library.yaml` (commented sample: the server's `library.example.yaml`), read by `readLibrary` (every check and refusal, with the reason), held in memory by `MeditateLibrary` and reread every ten minutes (`LibraryReloadFutureCall`, `startLibrary`); checked before deploying with `dart run bin/check_library.dart <folder>` in the server package. `LibraryEndpoint.catalogue` for signed-in people alone, with an access (`LibraryFileAccess`: the hour begun plus 12 hours, signed with a key made at start); the web server's `/library/<served name>?access=…` serves the files by ranges (`serveLibraryFile`), never by a path. In the app: Explore's section (none without a library), the item's page (recordings, Play, favourite as `library/<id>` in the settings, keep on the device, a sound under a silent sitting, its credit), the course's (lessons ticked), the player (a finished item is a sitting `library/<id>` and its lesson done), Settings' *Library credits*; the last catalogue in the person's folder (`library.json`), recordings kept in `library/` beside it. Tests: `test/feature/library/library_test.dart` and the app test's group, on `test/support/fake_library.dart` (made-up entries only: nothing of 7Mind's, whose structure alone D16 follows).
|
- **The operator's library** (D16; `lib/src/library/` on the server, `lib/src/feature/library/` in the app): a folder named by `MEDITATE_LIBRARY_DIR` with recordings, pictures and `library.yaml` (commented sample: the server's `library.example.yaml`), read by `readLibrary` (every check and refusal, with the reason), held in memory by `MeditateLibrary` and reread every ten minutes (`LibraryReloadFutureCall`, `startLibrary`); checked before deploying with `dart run bin/check_library.dart <folder>` in the server package. `LibraryEndpoint.catalogue` for signed-in people alone, with an access (`LibraryFileAccess`: the hour begun plus 12 hours, signed with a key made at start); the web server's `/library/<served name>?access=…` serves the files by ranges (`serveLibraryFile`), never by a path. In the app: Explore's section (none without a library), the item's page (recordings, Play, favourite as `library/<id>` in the settings, keep on the device, a sound under a silent sitting, its credit), the course's (lessons ticked), the player (a finished item is a sitting `library/<id>` and its lesson done), Settings' *Library credits*; the last catalogue in the person's folder (`library.json`), recordings kept in `library/` beside it. Tests: `test/feature/library/library_test.dart` and the app test's group, on `test/support/fake_library.dart` (made-up entries only: nothing of 7Mind's, whose structure alone D16 follows).
|
||||||
- **The bells** (D5) are `assets/bells/*.wav`, synthesised by `apps/meditate/tool/bells.py` (Python's standard library, run from `apps/meditate`; the same bytes every run) and played with `audioplayers` (`AudioplayersBellPlayer`). Only while the app runs: bells with the screen off are for later.
|
- **The bells** (D5) are `assets/bells/*.wav`, synthesised by `apps/meditate/tool/bells.py` (Python's standard library, run from `apps/meditate`; the same bytes every run) and played with `audioplayers` (`AudioplayersBellPlayer`). Only while the app runs: bells with the screen off are for later.
|
||||||
- **The icon** is Lucide's `sprout` from cobalt through pacific into fern (ADR 0031, D5, D7), drawn by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py meditate` from the root): `assets/icon/halkyon_meditate_icon.png`, Lucide's licence beside it, every platform's launcher icon.
|
- **The icon** is Lucide's `sprout` from cobalt through pacific into fern (ADR 0031, D5, D7), drawn by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py meditate` from the root): `assets/icon/halkyon_meditate_icon.png`, Lucide's licence beside it, every platform's launcher icon.
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ Future<void> main() async {
|
||||||
sharesStorage: device.storage.sharesWith,
|
sharesStorage: device.storage.sharesWith,
|
||||||
);
|
);
|
||||||
await instances.load(usedBefore: device.storage.usedBefore());
|
await instances.load(usedBefore: device.storage.usedBefore());
|
||||||
|
// The device's appearance and language, read before the first frame so
|
||||||
|
// that it opens in them.
|
||||||
|
final look = await LookCubit.open(
|
||||||
|
FileLookStore(File(p.join(device.support, 'look.json'))),
|
||||||
|
);
|
||||||
runApp(
|
runApp(
|
||||||
// What's new in each release (Train's ADR 0030, D61), from
|
// What's new in each release (Train's ADR 0030, D61), from
|
||||||
// assets/whats_new/, shown once after an update.
|
// assets/whats_new/, shown once after an update.
|
||||||
|
|
@ -45,6 +50,7 @@ Future<void> main() async {
|
||||||
// answers too, but holds nothing of this app's.
|
// answers too, but holds nothing of this app's.
|
||||||
checker: ServerpodInstanceChecker(app: 'meditate'),
|
checker: ServerpodInstanceChecker(app: 'meditate'),
|
||||||
open: (instance) => MeditateDependencies.create(instance, device),
|
open: (instance) => MeditateDependencies.create(instance, device),
|
||||||
|
look: look,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ import 'package:halkyon_meditate_flutter/src/feature/sitting/bloc/practice_cubit
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The device's look until Meditate has settings: the system's theme mode and
|
|
||||||
/// language.
|
|
||||||
const _systemLook = (themeMode: ThemeMode.system, locale: null);
|
|
||||||
|
|
||||||
/// Halkyon Meditate: halkyon_app's [InstanceGate] (the first start, the account
|
/// Halkyon Meditate: halkyon_app's [InstanceGate] (the first start, the account
|
||||||
/// on each instance) with Meditate's [meditateFrame], and on the instance in use
|
/// on each instance) with Meditate's [meditateFrame], and on the instance in use
|
||||||
/// halkyon_app's [SessionGate], which shows each signed-in person their own
|
/// halkyon_app's [SessionGate], which shows each signed-in person their own
|
||||||
|
|
@ -33,19 +29,27 @@ class const MeditateApp({
|
||||||
|
|
||||||
/// Meditate's dependencies on an instance.
|
/// Meditate's dependencies on an instance.
|
||||||
required final Future<MeditateDependencies> Function(Instance instance) open,
|
required final Future<MeditateDependencies> Function(Instance instance) open,
|
||||||
|
|
||||||
|
/// The device's appearance and language, chosen in Settings (ADR 0014's
|
||||||
|
/// amendment of 4 Nov 2026): above everything, as they apply before
|
||||||
|
/// anyone signs in.
|
||||||
|
required final LookCubit look,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => InstanceGate(
|
Widget build(BuildContext context) => BlocProvider.value(
|
||||||
frame: meditateFrame,
|
value: look,
|
||||||
choice: choice,
|
child: InstanceGate(
|
||||||
checker: checker,
|
frame: meditateFrame,
|
||||||
countryNames: countryNames,
|
choice: choice,
|
||||||
open: open,
|
checker: checker,
|
||||||
readLook: () async => _systemLook,
|
countryNames: countryNames,
|
||||||
app: SessionGate(
|
open: open,
|
||||||
look: (context) => _systemLook,
|
readLook: () async => look.state.settings,
|
||||||
app: (person) => MeditatePersonApp(person: person),
|
app: SessionGate(
|
||||||
|
look: (context) => context.watch<LookCubit>().state.settings,
|
||||||
|
app: (person) => MeditatePersonApp(person: person),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -74,57 +78,63 @@ class _MeditatePersonAppState() extends State<MeditatePersonApp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => MaterialApp.router(
|
Widget build(BuildContext context) {
|
||||||
routerConfig: _router,
|
// The device's look and language, chosen in Settings.
|
||||||
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
final look = context.watch<LookCubit>().state;
|
||||||
theme: appTheme(Brightness.light),
|
return MaterialApp.router(
|
||||||
darkTheme: appTheme(Brightness.dark),
|
routerConfig: _router,
|
||||||
localizationsDelegates: meditateLocalizationsDelegates,
|
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
theme: appTheme(Brightness.light),
|
||||||
localeListResolutionCallback: resolveAppLocale,
|
darkTheme: appTheme(Brightness.dark),
|
||||||
// How many accounts wait, for whoever operates the instance (Settings'
|
themeMode: look.themeMode,
|
||||||
// operator card and the badge on Profile and the cogs), and where sync
|
locale: look.locale,
|
||||||
// stands.
|
localizationsDelegates: meditateLocalizationsDelegates,
|
||||||
builder: (context, child) => PersonScope(
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
person: widget.person,
|
localeListResolutionCallback: resolveAppLocale,
|
||||||
child: MultiBlocProvider(
|
// How many accounts wait, for whoever operates the instance (Settings'
|
||||||
providers: [
|
// operator card and the badge on Profile and the cogs), and where sync
|
||||||
BlocProvider(
|
// stands.
|
||||||
create: (context) =>
|
builder: (context, child) => PersonScope(
|
||||||
OperatingCubit(AppScope.of(context).instanceOperators),
|
person: widget.person,
|
||||||
|
child: MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
OperatingCubit(AppScope.of(context).instanceOperators),
|
||||||
|
),
|
||||||
|
// The person's settings and sittings, for every tab and screen.
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) {
|
||||||
|
final person = PersonScope.of<MeditatePerson>(context);
|
||||||
|
return PracticeCubit(person.settings, person.sittings)..open();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// The person's dreams, for the Journal, Today and Profile (D14).
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => JournalCubit(
|
||||||
|
PersonScope.of<MeditatePerson>(context).dreams,
|
||||||
|
)..open(),
|
||||||
|
),
|
||||||
|
// The instance's library and the lessons done, for Explore, Today,
|
||||||
|
// Profile and Settings (D16).
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) {
|
||||||
|
final person = PersonScope.of<MeditatePerson>(context);
|
||||||
|
return LibraryCubit(person.library, person.lessons)..open();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Where sync stands: Settings' card and the dot on the cog.
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
SyncStatusBloc(PersonScope.of<MeditatePerson>(context).sync),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: ScreenColor(
|
||||||
|
color: PaletteColor.fern.background,
|
||||||
|
child: child!,
|
||||||
),
|
),
|
||||||
// The person's settings and sittings, for every tab and screen.
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) {
|
|
||||||
final person = PersonScope.of<MeditatePerson>(context);
|
|
||||||
return PracticeCubit(person.settings, person.sittings)..open();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// The person's dreams, for the Journal, Today and Profile (D14).
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) => JournalCubit(
|
|
||||||
PersonScope.of<MeditatePerson>(context).dreams,
|
|
||||||
)..open(),
|
|
||||||
),
|
|
||||||
// The instance's library and the lessons done, for Explore, Today,
|
|
||||||
// Profile and Settings (D16).
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) {
|
|
||||||
final person = PersonScope.of<MeditatePerson>(context);
|
|
||||||
return LibraryCubit(person.library, person.lessons)..open();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Where sync stands: Settings' card and the dot on the cog.
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
SyncStatusBloc(PersonScope.of<MeditatePerson>(context).sync),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: ScreenColor(
|
|
||||||
color: PaletteColor.fern.background,
|
|
||||||
child: child!,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,13 @@ const meditateLocalizationsDelegates = <LocalizationsDelegate<Object?>>[
|
||||||
HalkyonUiLocalizations.delegate,
|
HalkyonUiLocalizations.delegate,
|
||||||
...GlobalMaterialLocalizations.delegates,
|
...GlobalMaterialLocalizations.delegates,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The languages a person can choose for the app in Settings
|
||||||
|
/// (`DeviceLookCard`), in the order the picker lists them, each named in
|
||||||
|
/// its own language: every one the app speaks (`app_en_GB.arb`,
|
||||||
|
/// `app_en_US.arb`, `app_de.arb`).
|
||||||
|
const meditateLanguages = <AppLanguage>[
|
||||||
|
(tag: 'en-GB', name: 'English (UK)'),
|
||||||
|
(tag: 'en-US', name: 'English (US)'),
|
||||||
|
(tag: 'de', name: 'Deutsch'),
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ class const WeekCard({super.key}) extends StatelessWidget {
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(l10n.streakDays(figures.streak)),
|
// Wrapping where a language's words, or a large font,
|
||||||
|
// don't fit on one line.
|
||||||
|
Flexible(child: Text(l10n.streakDays(figures.streak))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/app/meditate_dependencies.dart';
|
import 'package:halkyon_meditate_flutter/src/app/meditate_dependencies.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/app/meditate_frame.dart';
|
import 'package:halkyon_meditate_flutter/src/app/meditate_frame.dart';
|
||||||
|
import 'package:halkyon_meditate_flutter/src/app/meditate_languages.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/app/meditate_paths.dart';
|
import 'package:halkyon_meditate_flutter/src/app/meditate_paths.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/feature/library/bloc/library_cubit.dart';
|
import 'package:halkyon_meditate_flutter/src/feature/library/bloc/library_cubit.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/feature/library/model/library.dart';
|
import 'package:halkyon_meditate_flutter/src/feature/library/model/library.dart';
|
||||||
|
|
@ -27,7 +28,7 @@ class const SettingsButton({super.key}) extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings, over the tabs (ADR 0038, D11), grouped in cards as Learn's and
|
/// Settings, over the tabs (ADR 0038, D11), grouped in cards as Learn's and
|
||||||
/// Train's: where sync stands; the operator's card for whoever operates the instance; Meditate's
|
/// Train's: where sync stands; the operator's card for whoever operates the instance; how this device shows the app (appearance, language); Meditate's
|
||||||
/// own groups (sitting, practice, journal); the account,
|
/// own groups (sitting, practice, journal); the account,
|
||||||
/// near the foot (halkyon_app's sections: whose it is on which instance,
|
/// near the foot (halkyon_app's sections: whose it is on which instance,
|
||||||
/// switching, signing out and deleting it, SimpleLogin, its password and
|
/// switching, signing out and deleting it, SimpleLogin, its password and
|
||||||
|
|
@ -66,6 +67,11 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
|
||||||
// the instance alone (Halkyon Learn's ADR 0021, D34),
|
// the instance alone (Halkyon Learn's ADR 0021, D34),
|
||||||
// first, as the cog's count leads here.
|
// first, as the cog's count leads here.
|
||||||
const InstanceOperatorCard(below: 12),
|
const InstanceOperatorCard(below: 12),
|
||||||
|
// How this device shows the app, its appearance and
|
||||||
|
// language, first of the app's own, as Learn's
|
||||||
|
// (halkyon_app's, ADR 0014's amendment of 4 Nov 2026).
|
||||||
|
const DeviceLookCard(languages: meditateLanguages),
|
||||||
|
const SizedBox(height: 12),
|
||||||
// Meditate's own groups (ADR 0038, D11), kept in the
|
// Meditate's own groups (ADR 0038, D11), kept in the
|
||||||
// person's sealed settings.
|
// person's sealed settings.
|
||||||
const SittingSettingsCard(),
|
const SittingSettingsCard(),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ import '../support/recording_device.dart';
|
||||||
void main() {
|
void main() {
|
||||||
final builtIn = Instance.parse('meditate.example.org')!;
|
final builtIn = Instance.parse('meditate.example.org')!;
|
||||||
late MemoryInstanceStore store;
|
late MemoryInstanceStore store;
|
||||||
|
|
||||||
|
/// The device's appearance and language, kept from start to start.
|
||||||
|
late MemoryLookStore looks;
|
||||||
late FakeAuthGateway gateway;
|
late FakeAuthGateway gateway;
|
||||||
late List<SignedIn> opened;
|
late List<SignedIn> opened;
|
||||||
late MemorySignInAddresses addresses;
|
late MemorySignInAddresses addresses;
|
||||||
|
|
@ -72,6 +75,7 @@ void main() {
|
||||||
bells = RecordingBells();
|
bells = RecordingBells();
|
||||||
screen = RecordingScreen();
|
screen = RecordingScreen();
|
||||||
store = MemoryInstanceStore();
|
store = MemoryInstanceStore();
|
||||||
|
looks = MemoryLookStore();
|
||||||
gateway = FakeAuthGateway();
|
gateway = FakeAuthGateway();
|
||||||
addresses = MemorySignInAddresses();
|
addresses = MemorySignInAddresses();
|
||||||
opened = [];
|
opened = [];
|
||||||
|
|
@ -109,10 +113,13 @@ void main() {
|
||||||
if (!usedBefore || signedOut) {
|
if (!usedBefore || signedOut) {
|
||||||
gateway.revokeSession();
|
gateway.revokeSession();
|
||||||
}
|
}
|
||||||
|
final look = await LookCubit.open(looks);
|
||||||
|
addTearDown(look.close);
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MeditateApp(
|
MeditateApp(
|
||||||
choice: choice,
|
choice: choice,
|
||||||
checker: FakeInstanceChecker({builtIn}),
|
checker: FakeInstanceChecker({builtIn}),
|
||||||
|
look: look,
|
||||||
countryNames: MemoryCountryNames(const {}),
|
countryNames: MemoryCountryNames(const {}),
|
||||||
open: (instance) async => MeditateDependencies(
|
open: (instance) async => MeditateDependencies(
|
||||||
serverName: instance.name,
|
serverName: instance.name,
|
||||||
|
|
@ -265,6 +272,45 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testWidgets("Settings choose the app's appearance and language, first of "
|
||||||
|
"Meditate's own: a choice applies at once and stays after a restart", (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
/// Opens the menu showing [current] and picks [choice].
|
||||||
|
Future<void> choose(String current, String choice) async {
|
||||||
|
await tester.tap(find.text(current));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text(choice).last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Brightness brightness() =>
|
||||||
|
Theme.of(tester.element(find.byType(Scaffold).last)).brightness;
|
||||||
|
|
||||||
|
await start(tester, usedBefore: true);
|
||||||
|
await openSettings(tester);
|
||||||
|
expect(find.text('Appearance'), findsOneWidget);
|
||||||
|
expect(find.text('Language'), findsOneWidget);
|
||||||
|
expect(brightness(), Brightness.light);
|
||||||
|
|
||||||
|
await choose('Device theme', 'Dark');
|
||||||
|
expect(brightness(), Brightness.dark);
|
||||||
|
|
||||||
|
await choose('Device language', 'Deutsch');
|
||||||
|
expect(find.widgetWithText(AppBar, 'Einstellungen'), findsOneWidget);
|
||||||
|
expect(find.text('Darstellung'), findsOneWidget);
|
||||||
|
expect(looks.look, const Look(appearance: Appearance.dark, language: 'de'));
|
||||||
|
|
||||||
|
// The app closed and opened again, on the look the device kept.
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await start(tester, usedBefore: true);
|
||||||
|
expect(brightness(), Brightness.dark);
|
||||||
|
expect(
|
||||||
|
Localizations.localeOf(tester.element(find.byType(Scaffold).last)),
|
||||||
|
const Locale('de'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets("a new install chooses its instance first, in Meditate's words "
|
testWidgets("a new install chooses its instance first, in Meditate's words "
|
||||||
'and look', (tester) async {
|
'and look', (tester) async {
|
||||||
await start(tester);
|
await start(tester);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue