feat(todo): Settings choose the app's appearance and language
The developer asked for Todo to offer what Learn's Settings offer first (ADR 0014's amendment of 4 Nov 2026). Todo 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 (todoLanguages). 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
23d3bcb4ca
commit
a2384dcbfe
6 changed files with 130 additions and 52 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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: 'todo'),
|
||||
open: (instance) => TodoDependencies.create(instance, device),
|
||||
look: look,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
|||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The device's look until Todo has settings: the system's theme mode and
|
||||
/// language.
|
||||
const _systemLook = (themeMode: ThemeMode.system, locale: null);
|
||||
|
||||
/// Halkyon Todo: halkyon_app's [InstanceGate] (the first start, the account
|
||||
/// on each instance) with Todo's [todoFrame], and on the instance in use
|
||||
/// halkyon_app's [SessionGate], which shows each signed-in person their own
|
||||
|
|
@ -32,19 +28,27 @@ class const TodoApp({
|
|||
|
||||
/// Todo's dependencies on an instance.
|
||||
required final Future<TodoDependencies> 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: todoFrame,
|
||||
choice: choice,
|
||||
checker: checker,
|
||||
countryNames: countryNames,
|
||||
open: open,
|
||||
readLook: () async => _systemLook,
|
||||
app: SessionGate(
|
||||
look: (context) => _systemLook,
|
||||
app: (person) => TodoPersonApp(person: person),
|
||||
Widget build(BuildContext context) => BlocProvider.value(
|
||||
value: look,
|
||||
child: InstanceGate(
|
||||
frame: todoFrame,
|
||||
choice: choice,
|
||||
checker: checker,
|
||||
countryNames: countryNames,
|
||||
open: open,
|
||||
readLook: () async => look.state.settings,
|
||||
app: SessionGate(
|
||||
look: (context) => context.watch<LookCubit>().state.settings,
|
||||
app: (person) => TodoPersonApp(person: person),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -73,43 +77,49 @@ class _TodoPersonAppState() extends State<TodoPersonApp> {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
||||
theme: appTheme(Brightness.light),
|
||||
darkTheme: appTheme(Brightness.dark),
|
||||
localizationsDelegates: todoLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: resolveAppLocale,
|
||||
// How many accounts wait, for whoever operates the instance (Settings'
|
||||
// operator card and the badge on Browse and its cog), 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: todoLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: resolveAppLocale,
|
||||
// How many accounts wait, for whoever operates the instance (Settings'
|
||||
// operator card and the badge on Browse and its cog), 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 projects, tasks and labels, for every tab and
|
||||
// sheet.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
TodoCubit(PersonScope.of<TodoPerson>(context))..open(),
|
||||
),
|
||||
// Where sync stands: Settings' card and the dot on the cog.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
SyncStatusBloc(PersonScope.of<TodoPerson>(context).sync),
|
||||
),
|
||||
],
|
||||
child: ScreenColor(
|
||||
color: PaletteColor.strawberry.background,
|
||||
// Their tasks' reminders on this device (ADR 0033, D5).
|
||||
child: ReminderScheduler(child: child!),
|
||||
),
|
||||
// The person's projects, tasks and labels, for every tab and
|
||||
// sheet.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
TodoCubit(PersonScope.of<TodoPerson>(context))..open(),
|
||||
),
|
||||
// Where sync stands: Settings' card and the dot on the cog.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
SyncStatusBloc(PersonScope.of<TodoPerson>(context).sync),
|
||||
),
|
||||
],
|
||||
child: ScreenColor(
|
||||
color: PaletteColor.strawberry.background,
|
||||
// Their tasks' reminders on this device (ADR 0033, D5).
|
||||
child: ReminderScheduler(child: child!),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,3 +13,13 @@ const todoLocalizationsDelegates = <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 todoLanguages = <AppLanguage>[
|
||||
(tag: 'en-GB', name: 'English (UK)'),
|
||||
(tag: 'en-US', name: 'English (US)'),
|
||||
(tag: 'de', name: 'Deutsch'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:halkyon_app/halkyon_app.dart';
|
|||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_languages.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_paths.dart';
|
||||
import 'package:halkyon_todo_flutter/src/settings/todo_settings_cards.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
|
@ -24,7 +25,7 @@ class const SettingsButton({super.key}) extends StatelessWidget {
|
|||
}
|
||||
|
||||
/// Settings, over the tabs (ADR 0033, D4), grouped in cards as Learn's and
|
||||
/// Train's: where sync stands; the operator's card for whoever operates the instance; Todo's
|
||||
/// Train's: where sync stands; the operator's card for whoever operates the instance; how this device shows the app (appearance, language); Todo's
|
||||
/// own groups as their settings come (none is shown empty); 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
|
||||
|
|
@ -63,6 +64,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: todoLanguages),
|
||||
const SizedBox(height: 12),
|
||||
// Todo's own groups (ADR 0033, D4), kept in the person's
|
||||
// sealed settings.
|
||||
const TaskSettingsCard(),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ import '../support/memory_sealed_row_gateway.dart';
|
|||
void main() {
|
||||
final builtIn = Instance.parse('todo.example.org')!;
|
||||
late MemoryInstanceStore store;
|
||||
|
||||
/// The device's appearance and language, kept from start to start.
|
||||
late MemoryLookStore looks;
|
||||
late FakeAuthGateway gateway;
|
||||
late List<SignedIn> opened;
|
||||
late MemorySignInAddresses addresses;
|
||||
|
|
@ -83,6 +86,7 @@ void main() {
|
|||
settingsRows = MemorySealedRowGateway();
|
||||
reminders = MemoryReminderGateway();
|
||||
store = MemoryInstanceStore();
|
||||
looks = MemoryLookStore();
|
||||
gateway = FakeAuthGateway();
|
||||
addresses = MemorySignInAddresses();
|
||||
opened = [];
|
||||
|
|
@ -111,10 +115,13 @@ void main() {
|
|||
if (!usedBefore || signedOut) {
|
||||
gateway.revokeSession();
|
||||
}
|
||||
final look = await LookCubit.open(looks);
|
||||
addTearDown(look.close);
|
||||
await tester.pumpWidget(
|
||||
TodoApp(
|
||||
choice: choice,
|
||||
checker: FakeInstanceChecker({builtIn}),
|
||||
look: look,
|
||||
countryNames: MemoryCountryNames(const {}),
|
||||
open: (instance) async => TodoDependencies(
|
||||
serverName: instance.name,
|
||||
|
|
@ -228,6 +235,45 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets("Settings choose the app's appearance and language, first of "
|
||||
"Todo'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 Todo's words and "
|
||||
'look', (tester) async {
|
||||
await start(tester);
|
||||
|
|
|
|||
Loading…
Reference in a new issue