feat(app,learn): the device's appearance and language, halkyon_app's, as Learn's Settings show them
The developer asked for Todo, Train and Meditate to offer Learn's
Appearance and Language (ADR 0014's amendment of 4 Nov 2026). Their
model, store, cubit and card move into halkyon_app: Appearance and Look
(a BCP 47 tag, null for the device's language), FileLookStore keeping it
in the app's support folder as JSON written by rename (per device, not
synced, read before the first frame), LookCubit for the apps to provide
above InstanceGate, and LookSettingsCard / DeviceLookCard with the texts
moved from Learn's arbs (look*, the same words). Each app passes the
languages it speaks, named in their own language.
Learn keeps its stored choice where it was, among its DeviceSettings,
and feeds the shared card from them: no migration, so nobody's language
or appearance moves, and nothing changes on its screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 22:30:29 +01:00
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
|
|
|
import 'package:halkyon_app/testing.dart';
|
|
|
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late Directory folder;
|
|
|
|
|
late File file;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
folder = Directory.systemTemp.createTempSync('look');
|
|
|
|
|
file = File('${folder.path}/look.json');
|
|
|
|
|
});
|
|
|
|
|
tearDown(() => folder.deleteSync(recursive: true));
|
|
|
|
|
|
|
|
|
|
test("the device's look before anything is kept", () async {
|
|
|
|
|
expect(await FileLookStore(file).read(), const Look());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('keeps the appearance and language for the next start', () async {
|
|
|
|
|
const look = Look(appearance: Appearance.dark, language: 'de');
|
|
|
|
|
await FileLookStore(file).write(look);
|
|
|
|
|
|
|
|
|
|
expect(await FileLookStore(file).read(), look);
|
|
|
|
|
|
|
|
|
|
await FileLookStore(file).write(const Look(appearance: Appearance.light));
|
|
|
|
|
expect(
|
|
|
|
|
await FileLookStore(file).read(),
|
|
|
|
|
const Look(appearance: Appearance.light),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
feat: the rail opens into a sidebar with labels from expanded windows on
Decided by the developer on 9 and 10 Nov 2026: phones keep the bar,
medium windows (600 to 840 dp) the rail alone, and from 840 dp a toggle
at the rail's foot opens it into a sidebar 256 dp wide, as Proton
Drive's: the app named beside its icon, each tab its label beside its
icon, the icons where the rail had them, so nothing moves under the
pointer. Its labels come in one after another, top to bottom, all at once
where animations are off. Open or closed is kept per device, in
halkyon_app's Look (look.json, `sidebar`), which Learn keeps in its
DeviceSettings (`sidebarOpen`), as it does the appearance and language.
NavigationFrame takes sidebarOpen and onSidebarChanged (no toggle without
it), and sidebarSections, what the app lists below the tabs, which Todo's
favourites and projects will be. The closed rail keeps Flutter's widget,
its toggle pinned at the foot (trailingAtBottom); the open one is the
frame's own, as the staggered labels need. Learn, Todo, Train and
Meditate keep the choice.
Whats-New: In wide windows, the button at the foot of the side rail opens it into a sidebar with names, and it stays open.
Whats-New-De: In breiten Fenstern öffnet der Knopf unten an der Seitenleiste sie mit Namen, und sie bleibt offen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-10 16:44:08 +01:00
|
|
|
test('keeps the sidebar open or closed (decided 10 Nov 2026), the '
|
|
|
|
|
'appearance and language as they were', () async {
|
|
|
|
|
const look = Look(appearance: Appearance.dark, sidebarOpen: true);
|
|
|
|
|
await FileLookStore(file).write(look);
|
|
|
|
|
expect(await FileLookStore(file).read(), look);
|
|
|
|
|
|
|
|
|
|
final cubit = await LookCubit.open(FileLookStore(file));
|
|
|
|
|
addTearDown(cubit.close);
|
|
|
|
|
await cubit.chooseSidebar(open: false);
|
|
|
|
|
expect(
|
|
|
|
|
await FileLookStore(file).read(),
|
|
|
|
|
const Look(appearance: Appearance.dark),
|
|
|
|
|
);
|
|
|
|
|
await cubit.chooseLanguage('de');
|
|
|
|
|
expect(cubit.state.sidebarOpen, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a file from before the sidebar reads it closed', () async {
|
|
|
|
|
file.writeAsStringSync('{"appearance": "light"}');
|
|
|
|
|
expect(
|
|
|
|
|
await FileLookStore(file).read(),
|
|
|
|
|
const Look(appearance: Appearance.light),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
feat(app,learn): the device's appearance and language, halkyon_app's, as Learn's Settings show them
The developer asked for Todo, Train and Meditate to offer Learn's
Appearance and Language (ADR 0014's amendment of 4 Nov 2026). Their
model, store, cubit and card move into halkyon_app: Appearance and Look
(a BCP 47 tag, null for the device's language), FileLookStore keeping it
in the app's support folder as JSON written by rename (per device, not
synced, read before the first frame), LookCubit for the apps to provide
above InstanceGate, and LookSettingsCard / DeviceLookCard with the texts
moved from Learn's arbs (look*, the same words). Each app passes the
languages it speaks, named in their own language.
Learn keeps its stored choice where it was, among its DeviceSettings,
and feeds the shared card from them: no migration, so nobody's language
or appearance moves, and nothing changes on its screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 22:30:29 +01:00
|
|
|
test("a damaged file or an unknown value reads as the device's", () async {
|
|
|
|
|
file.writeAsStringSync('{"appearance": "dark", ');
|
|
|
|
|
expect(await FileLookStore(file).read(), const Look());
|
|
|
|
|
|
|
|
|
|
file.writeAsStringSync('[1, 2]');
|
|
|
|
|
expect(await FileLookStore(file).read(), const Look());
|
|
|
|
|
|
|
|
|
|
file.writeAsStringSync('{"appearance": "sepia", "language": 7}');
|
|
|
|
|
expect(await FileLookStore(file).read(), const Look());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a look means a theme mode and a locale', () {
|
|
|
|
|
expect(const Look().themeMode, ThemeMode.system);
|
|
|
|
|
expect(const Look().locale, isNull);
|
|
|
|
|
expect(const Look(appearance: Appearance.dark).themeMode, ThemeMode.dark);
|
|
|
|
|
expect(const Look(appearance: Appearance.light).themeMode, ThemeMode.light);
|
|
|
|
|
expect(const Look(language: 'de').locale, const Locale('de'));
|
|
|
|
|
expect(const Look(language: 'en-US').locale, const Locale('en', 'US'));
|
|
|
|
|
expect(
|
|
|
|
|
const Look(appearance: Appearance.dark, language: 'de').settings,
|
|
|
|
|
(themeMode: ThemeMode.dark, locale: const Locale('de')),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a choice applies at once and is kept', () async {
|
|
|
|
|
final store = MemoryLookStore();
|
|
|
|
|
final cubit = await LookCubit.open(store);
|
|
|
|
|
addTearDown(cubit.close);
|
|
|
|
|
|
|
|
|
|
await cubit.chooseAppearance(Appearance.dark);
|
|
|
|
|
await cubit.chooseLanguage('de');
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
const Look(appearance: Appearance.dark, language: 'de'),
|
|
|
|
|
);
|
|
|
|
|
expect(store.look, cubit.state);
|
|
|
|
|
|
|
|
|
|
await cubit.chooseLanguage(null);
|
|
|
|
|
expect(store.look, const Look(appearance: Appearance.dark));
|
|
|
|
|
});
|
|
|
|
|
}
|