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:
Tizian 「ティツィアーン」 2026-11-04 22:30:29 +01:00
commit bf9818e22d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 147 additions and 67 deletions

View file

@ -28,6 +28,11 @@ Future<void> main() async {
sharesStorage: device.storage.sharesWith,
);
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(
// What's new in each release (Train's ADR 0030, D61), from
// 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.
checker: ServerpodInstanceChecker(app: 'meditate'),
open: (instance) => MeditateDependencies.create(instance, device),
look: look,
),
),
);

View file

@ -11,10 +11,6 @@ import 'package:halkyon_meditate_flutter/src/feature/sitting/bloc/practice_cubit
import 'package:halkyon_ui/halkyon_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
/// 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
@ -33,19 +29,27 @@ class const MeditateApp({
/// Meditate's dependencies on an instance.
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,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) => InstanceGate(
frame: meditateFrame,
choice: choice,
checker: checker,
countryNames: countryNames,
open: open,
readLook: () async => _systemLook,
app: SessionGate(
look: (context) => _systemLook,
app: (person) => MeditatePersonApp(person: person),
Widget build(BuildContext context) => BlocProvider.value(
value: look,
child: InstanceGate(
frame: meditateFrame,
choice: choice,
checker: checker,
countryNames: countryNames,
open: open,
readLook: () async => look.state.settings,
app: SessionGate(
look: (context) => context.watch<LookCubit>().state.settings,
app: (person) => MeditatePersonApp(person: person),
),
),
);
}
@ -74,57 +78,63 @@ class _MeditatePersonAppState() extends State<MeditatePersonApp> {
}
@override
Widget build(BuildContext context) => MaterialApp.router(
routerConfig: _router,
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
theme: appTheme(Brightness.light),
darkTheme: appTheme(Brightness.dark),
localizationsDelegates: meditateLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: resolveAppLocale,
// How many accounts wait, for whoever operates the instance (Settings'
// operator card and the badge on Profile and the cogs), and where sync
// stands.
builder: (context, child) => PersonScope(
person: widget.person,
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
OperatingCubit(AppScope.of(context).instanceOperators),
Widget build(BuildContext context) {
// The device's look and language, chosen in Settings.
final look = context.watch<LookCubit>().state;
return MaterialApp.router(
routerConfig: _router,
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
theme: appTheme(Brightness.light),
darkTheme: appTheme(Brightness.dark),
themeMode: look.themeMode,
locale: look.locale,
localizationsDelegates: meditateLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: resolveAppLocale,
// How many accounts wait, for whoever operates the instance (Settings'
// operator card and the badge on Profile and the cogs), and where sync
// stands.
builder: (context, child) => PersonScope(
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!,
),
),
),
);
);
}
}

View file

@ -13,3 +13,13 @@ const meditateLocalizationsDelegates = <LocalizationsDelegate<Object?>>[
HalkyonUiLocalizations.delegate,
...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'),
];

View file

@ -177,7 +177,9 @@ class const WeekCard({super.key}) extends StatelessWidget {
color: theme.colorScheme.primary,
),
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))),
],
),
),

View file

@ -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/src/app/meditate_dependencies.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/feature/library/bloc/library_cubit.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
/// 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,
/// near the foot (halkyon_app's sections: whose it is on which instance,
/// 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),
// first, as the cog's count leads here.
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
// person's sealed settings.
const SittingSettingsCard(),