refactor(halkyon_app): the account section, About, the language and countries' names, shared
What Learn and Todo each held now comes from halkyon_app: - AccountTiles: whose account on which instance, switching, signing out (asking first for an anonymous account, as it deletes it) and deleting it (asking first), the app's own words passed in. - showAppAbout: the app's name, version, icon and legalese as it gives them, the source of what the instance runs (AppDependencies gains sourceCodeUrl), the app's own parts, the licence list. - resolveAppLocale: the first preferred language the app speaks, never by country alone, British English otherwise. - AssetCountryNames, the default of InstanceGate and InstanceScope: Unicode CLDR 48.2's territory names in English and German (tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and Palestine by their short names as Learn's world map has them, so Todo says "Germany (EU)" where it said "DE (EU)". Learn's texts read as before in every language; its About bloc and gateway, its country names from the world map and the keys only the moved code used are gone, as are Todo's copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
65c5cd740d
commit
87500b7c6b
13 changed files with 1001 additions and 834 deletions
|
|
@ -4,6 +4,6 @@ What holds for every app and package of this repository is in the [root AGENTS.m
|
|||
|
||||
- **A shell on the shared packages** (decided 2026-10-19): signing in, the instances, the account and the look work; nothing of a person's is kept or synced yet, which waits for Todo's first model (the developer decides what it keeps and syncs). Packages: `halkyon_todo_server`, `halkyon_todo_client` (generated, never edited), `halkyon_todo_flutter`; app ID `app.halkyon.todo` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`).
|
||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`todoBase()`, `lib/src/todo_base.dart`): its basics say it serves `todo` (never change it once apps use it), sign-in mails name Halkyon Todo, the source is this repository, `minimumAppBuild` 1, no data of its own to delete with a person yet (add a `deletePersonData` hook with Todo's first table). Authentication through the module's builders: anonymous, GitHub (with the instance's own OAuth app) and email codes, server-side sessions. **Accounts** (ADR 0016): Todo holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`todoBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone (GitHub's are Todo's own OAuth app's), and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts: anonymous people daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. Migrations (2026-10-19): the initial one (Serverpod's tables, its auth modules' and the module's `email_identity`, `email_code_request`), `profiles` (`public_profile`) and `deletion-notices` (`account_deletion_notice`), nothing of Todo's own. Tests (`test/integration/todo_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
|
||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Todo's frame (`todoFrame`: "Todo" under "Halkyon", its icon, `todo.example.org`, its own three sentences) and `ServerpodInstanceChecker(app: 'todo')`; `TodoDependencies` (`AppDependencies<TodoPerson>`) on one instance, `TodoPerson` (empty) per person; a person's app with an empty home and the account sheet (whose account on which instance, Switch account, Sign out, which asks first for an anonymous account as it deletes it, Delete account, which asks first, and About with the source of what the instance runs and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Todo has settings. Countries in where an instance keeps its data show by their codes until halkyon_app names countries itself. Widget tests start Todo's whole tree with halkyon_app's fakes (`test/app/todo_app_test.dart`).
|
||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Todo's frame (`todoFrame`: "Todo" under "Halkyon", its icon, `todo.example.org`, its own three sentences) and `ServerpodInstanceChecker(app: 'todo')`; `TodoDependencies` (`AppDependencies<TodoPerson>`) on one instance, `TodoPerson` (empty) per person; a person's app with an empty home and the account sheet: halkyon_app's `AccountTiles` (whose account on which instance, Switch account, Sign out, which asks first for an anonymous account as it deletes it, Delete account, which asks first) in Todo's words, and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TodoDependencies.sourceCodeUrl`, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Todo has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Todo's whole tree with halkyon_app's fakes (`test/app/todo_app_test.dart`).
|
||||
- **The icon** is Lucide's `list-checks` (lucide-static 1.50.0) in the brand's blues on white: `apps/todo/tool/app_icons.py` (run from `apps/todo`) fetches it, draws its strokes with Pillow (no SVG renderer needed; other path commands fail loudly), writes `assets/icon/halkyon_todo_icon.png` and Lucide's ISC licence beside it, then every platform's launcher icon through flutter_launcher_icons as Learn's tool does. `registerLucideLicense` puts the notice in the licence list.
|
||||
- **`serverpod start`** runs in `apps/todo/halkyon_todo_server` (it launches the app from `lib/driver.dart`); `.mcp.json`'s `--server-dir` names it when a session works on Todo. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Learn's and Todo's development servers use the same ports: run one at a time.
|
||||
|
|
|
|||
|
|
@ -12,16 +12,8 @@
|
|||
"homeEmptyHint": "Aufgaben kommen mit einer späteren Version von Halkyon Todo.",
|
||||
|
||||
"accountTitle": "Konto",
|
||||
"accountSignedIn": "Angemeldet auf {instance}",
|
||||
"accountAnonymous": "Anonym auf {instance}",
|
||||
"accountSwitch": "Konto wechseln",
|
||||
"accountSignOut": "Abmelden",
|
||||
"accountSignOutTitle": "Abmelden?",
|
||||
"accountSignOutAnonymousMessage": "Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden gelöscht.",
|
||||
"accountDelete": "Konto löschen",
|
||||
"accountDeleteTitle": "Dein Konto löschen?",
|
||||
"accountDeleteMessage": "Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.",
|
||||
|
||||
"aboutTitle": "Über",
|
||||
"aboutSource": "Der Quellcode dessen, was diese Instanz betreibt:"
|
||||
"aboutTitle": "Über"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,18 +15,8 @@
|
|||
"homeEmptyHint": "Tasks come with a later version of Halkyon Todo.",
|
||||
|
||||
"accountTitle": "Account",
|
||||
"accountSignedIn": "Signed in on {instance}",
|
||||
"@accountSignedIn": {"placeholders": {"instance": {"type": "String"}}},
|
||||
"accountAnonymous": "Anonymous on {instance}",
|
||||
"@accountAnonymous": {"placeholders": {"instance": {"type": "String"}}},
|
||||
"accountSwitch": "Switch account",
|
||||
"accountSignOut": "Sign out",
|
||||
"accountSignOutTitle": "Sign out?",
|
||||
"accountSignOutAnonymousMessage": "This account can't be signed into again, so signing out deletes it.",
|
||||
"accountDelete": "Delete account",
|
||||
"accountDeleteTitle": "Delete your account?",
|
||||
"accountDeleteMessage": "Your account is deleted on the server and on all your devices. This can't be undone.",
|
||||
|
||||
"aboutTitle": "About",
|
||||
"aboutSource": "The source of what this instance runs:"
|
||||
"aboutTitle": "About"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,54 +148,12 @@ abstract class AppLocalizations {
|
|||
/// **'Account'**
|
||||
String get accountTitle;
|
||||
|
||||
/// No description provided for @accountSignedIn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signed in on {instance}'**
|
||||
String accountSignedIn(String instance);
|
||||
|
||||
/// No description provided for @accountAnonymous.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Anonymous on {instance}'**
|
||||
String accountAnonymous(String instance);
|
||||
|
||||
/// No description provided for @accountSwitch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Switch account'**
|
||||
String get accountSwitch;
|
||||
|
||||
/// No description provided for @accountSignOut.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign out'**
|
||||
String get accountSignOut;
|
||||
|
||||
/// No description provided for @accountSignOutTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign out?'**
|
||||
String get accountSignOutTitle;
|
||||
|
||||
/// No description provided for @accountSignOutAnonymousMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This account can\'t be signed into again, so signing out deletes it.'**
|
||||
String get accountSignOutAnonymousMessage;
|
||||
|
||||
/// No description provided for @accountDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete account'**
|
||||
String get accountDelete;
|
||||
|
||||
/// No description provided for @accountDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete your account?'**
|
||||
String get accountDeleteTitle;
|
||||
|
||||
/// No description provided for @accountDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -207,12 +165,6 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'About'**
|
||||
String get aboutTitle;
|
||||
|
||||
/// No description provided for @aboutSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The source of what this instance runs:'**
|
||||
String get aboutSource;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -37,42 +37,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get accountTitle => 'Konto';
|
||||
|
||||
@override
|
||||
String accountSignedIn(String instance) {
|
||||
return 'Angemeldet auf $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String accountAnonymous(String instance) {
|
||||
return 'Anonym auf $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String get accountSwitch => 'Konto wechseln';
|
||||
|
||||
@override
|
||||
String get accountSignOut => 'Abmelden';
|
||||
|
||||
@override
|
||||
String get accountSignOutTitle => 'Abmelden?';
|
||||
|
||||
@override
|
||||
String get accountSignOutAnonymousMessage =>
|
||||
'Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden gelöscht.';
|
||||
|
||||
@override
|
||||
String get accountDelete => 'Konto löschen';
|
||||
|
||||
@override
|
||||
String get accountDeleteTitle => 'Dein Konto löschen?';
|
||||
|
||||
@override
|
||||
String get accountDeleteMessage =>
|
||||
'Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.';
|
||||
|
||||
@override
|
||||
String get aboutTitle => 'Über';
|
||||
|
||||
@override
|
||||
String get aboutSource => 'Der Quellcode dessen, was diese Instanz betreibt:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,44 +37,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get accountTitle => 'Account';
|
||||
|
||||
@override
|
||||
String accountSignedIn(String instance) {
|
||||
return 'Signed in on $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String accountAnonymous(String instance) {
|
||||
return 'Anonymous on $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String get accountSwitch => 'Switch account';
|
||||
|
||||
@override
|
||||
String get accountSignOut => 'Sign out';
|
||||
|
||||
@override
|
||||
String get accountSignOutTitle => 'Sign out?';
|
||||
|
||||
@override
|
||||
String get accountSignOutAnonymousMessage =>
|
||||
'This account can\'t be signed into again, so signing out deletes it.';
|
||||
|
||||
@override
|
||||
String get accountDelete => 'Delete account';
|
||||
|
||||
@override
|
||||
String get accountDeleteTitle => 'Delete your account?';
|
||||
|
||||
@override
|
||||
String get accountDeleteMessage =>
|
||||
'Your account is deleted on the server and on all your devices. This can\'t be undone.';
|
||||
|
||||
@override
|
||||
String get aboutTitle => 'About';
|
||||
|
||||
@override
|
||||
String get aboutSource => 'The source of what this instance runs:';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -32,9 +32,6 @@ Future<void> main() async {
|
|||
// Only an instance of Halkyon Todo: one of another Halkyon app's
|
||||
// answers too, but holds nothing of this app's.
|
||||
checker: ServerpodInstanceChecker(app: 'todo'),
|
||||
// No country names yet: where an instance keeps people's data shows
|
||||
// its flag and the country's code.
|
||||
countryNames: MemoryCountryNames(const {}),
|
||||
open: (instance) => TodoDependencies.create(instance, device),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
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_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The account, as Learn's Settings shows it: whose it is, on which instance,
|
||||
/// switching to the account on another, signing out and deleting it; and
|
||||
/// About.
|
||||
/// The account: halkyon_app's section (whose it is, on which instance,
|
||||
/// switching to the account on another, signing out and deleting it) in
|
||||
/// Todo's words where only Todo can say it; and About.
|
||||
Future<void> showAccountSheet(BuildContext context) => showAdaptiveSheet(
|
||||
context,
|
||||
builder: (context) => const _AccountSheet(),
|
||||
|
|
@ -21,107 +17,29 @@ class const _AccountSheet() extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final instance = TodoDependencies.of(context).serverName;
|
||||
final identified = switch (context.watch<SessionBloc>().state) {
|
||||
SessionStateActive(:final person) => person.isIdentified,
|
||||
_ => false,
|
||||
};
|
||||
return SheetScaffold(
|
||||
title: l10n.accountTitle,
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
identified ? LucideIcons.userCheck : LucideIcons.userRound,
|
||||
),
|
||||
title: Text(
|
||||
identified
|
||||
? l10n.accountSignedIn(instance)
|
||||
: l10n.accountAnonymous(instance),
|
||||
),
|
||||
),
|
||||
// The accounts on this device, one per instance.
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.users),
|
||||
title: Text(l10n.accountSwitch),
|
||||
onTap: () => showAccountSwitcher(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.logOut),
|
||||
title: Text(l10n.accountSignOut),
|
||||
onTap: () => _signOut(context, identified: identified),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.trash2, color: theme.colorScheme.error),
|
||||
title: Text(
|
||||
l10n.accountDelete,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
onTap: () => _delete(context),
|
||||
AccountTiles(
|
||||
signOutAnonymousMessage: l10n.accountSignOutAnonymousMessage,
|
||||
deleteMessage: l10n.accountDeleteMessage,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.info),
|
||||
title: Text(l10n.aboutTitle),
|
||||
onTap: () => unawaited(_showAbout(context)),
|
||||
// halkyon_app's About: the app's name and icon, the source of
|
||||
// what the instance runs, which both licences ask to offer, and
|
||||
// the licence list.
|
||||
onTap: () => showAppAbout(
|
||||
context,
|
||||
icon: const Image(image: todoIcon, width: 48, height: 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs out; an anonymous account can't be signed into again, so signing out
|
||||
/// of one deletes it, and asks first. The app then asks how to start.
|
||||
Future<void> _signOut(BuildContext context, {required bool identified}) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final session = context.read<SessionBloc>();
|
||||
if (!identified &&
|
||||
!await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.accountSignOutTitle,
|
||||
message: l10n.accountSignOutAnonymousMessage,
|
||||
confirmLabel: l10n.accountSignOut,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
session.add(const SessionEventSignOutRequested());
|
||||
}
|
||||
|
||||
/// Deletes the account with everything in it, after asking.
|
||||
Future<void> _delete(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final session = context.read<SessionBloc>();
|
||||
if (await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.accountDeleteTitle,
|
||||
message: l10n.accountDeleteMessage,
|
||||
confirmLabel: l10n.accountDelete,
|
||||
)) {
|
||||
session.add(const SessionEventDeleteRequested());
|
||||
}
|
||||
}
|
||||
|
||||
/// The app's About: its name and icon, the source of what the instance runs
|
||||
/// (as its basics name it, which both licences ask to offer), and the licence
|
||||
/// list.
|
||||
Future<void> _showAbout(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final source = await TodoDependencies.of(context).sourceCode();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: l10n.appTitle,
|
||||
applicationIcon: const Image(image: todoIcon, width: 48, height: 48),
|
||||
children: [
|
||||
if (source != null) ...[
|
||||
Text(l10n.aboutSource),
|
||||
SelectableText('$source'),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ class const TodoApp({
|
|||
/// Todo's.
|
||||
required final InstanceChecker checker,
|
||||
|
||||
/// Names the countries instances keep their data in.
|
||||
required final CountryNames countryNames,
|
||||
/// Names the countries instances keep their data in: Unicode CLDR's,
|
||||
/// halkyon_app's, unless a test gives others.
|
||||
final CountryNames countryNames = const AssetCountryNames(),
|
||||
|
||||
/// Todo's dependencies on an instance.
|
||||
required final Future<TodoDependencies> Function(Instance instance) open,
|
||||
|
|
@ -60,7 +61,7 @@ class const TodoPersonApp({
|
|||
darkTheme: appTheme(Brightness.dark),
|
||||
localizationsDelegates: todoLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: resolveTodoLocale,
|
||||
localeListResolutionCallback: resolveAppLocale,
|
||||
builder: (context, child) => PersonScope(person: person, child: child!),
|
||||
home: const HomeScreen(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ class TodoDependencies({
|
|||
@override required final SignedOutMark signedOut,
|
||||
|
||||
/// Where the source of what the instance runs is, as its basics name it
|
||||
/// (halkyon_base): both licences ask to offer it, which About does.
|
||||
required final Future<Uri?> Function() sourceCode,
|
||||
/// (halkyon_base): both licences ask to offer it, which About does
|
||||
/// ([sourceCodeUrl]).
|
||||
required final Future<String> Function() _sourceCodeUrl,
|
||||
|
||||
/// Opens a signed-in person's dependencies ([openPerson]).
|
||||
required final OpenTodoPerson _openPerson,
|
||||
|
|
@ -50,6 +51,9 @@ class TodoDependencies({
|
|||
@override
|
||||
Future<TodoPerson> openPerson(SignedIn person) => _openPerson(person);
|
||||
|
||||
@override
|
||||
Future<String> sourceCodeUrl() => _sourceCodeUrl();
|
||||
|
||||
/// Builds the production graph on [instance], around one Serverpod client
|
||||
/// of its own, with what [device] shares across instances. Its session and
|
||||
/// its people's folders are the instance's own (`InstanceStorage`); the
|
||||
|
|
@ -83,9 +87,8 @@ class TodoDependencies({
|
|||
serverName: instance.name,
|
||||
auth: AuthRepository(authGateway, FileSignInAddresses(people)),
|
||||
signedOut: FileSignedOutMark(people),
|
||||
sourceCode: () async => Uri.tryParse(
|
||||
(await client.modules.base.instance.basics()).sourceCodeUrl,
|
||||
),
|
||||
sourceCodeUrl: () async =>
|
||||
(await client.modules.base.instance.basics()).sourceCodeUrl,
|
||||
openPerson: (person) async {
|
||||
// One person's folder on the device at a time, as Learn keeps it.
|
||||
await folders.openOnly(person.authUserId);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const todoFrame = AppFrame(
|
|||
theme: appTheme,
|
||||
localizationsDelegates: todoLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: resolveTodoLocale,
|
||||
localeListResolutionCallback: resolveAppLocale,
|
||||
launchScreen: TodoLaunchScreen(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,40 +13,3 @@ const todoLocalizationsDelegates = <LocalizationsDelegate<Object?>>[
|
|||
HalkyonUiLocalizations.delegate,
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
];
|
||||
|
||||
/// The English the app speaks wherever it can't speak the language asked for:
|
||||
/// British English, which `app_en.arb` holds.
|
||||
const _fallback = Locale('en', 'GB');
|
||||
|
||||
/// Which of the [supported] locales the app speaks, for a device preferring
|
||||
/// the [preferred] ones in this order, as Learn picks (`resolveAppLocale`):
|
||||
/// the first language it speaks wins, in its country's variant where there is
|
||||
/// one, English of any other country as British English, and a country alone
|
||||
/// never picks a language.
|
||||
Locale resolveTodoLocale(
|
||||
List<Locale>? preferred,
|
||||
Iterable<Locale> supported,
|
||||
) {
|
||||
for (final locale in preferred ?? const <Locale>[]) {
|
||||
final sameLanguage = supported.where(
|
||||
(candidate) => candidate.languageCode == locale.languageCode,
|
||||
);
|
||||
if (sameLanguage.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
for (final candidate in sameLanguage) {
|
||||
if (candidate.countryCode != null &&
|
||||
candidate.countryCode == locale.countryCode) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (locale.languageCode == _fallback.languageCode) {
|
||||
return _fallback;
|
||||
}
|
||||
return sameLanguage.firstWhere(
|
||||
(candidate) => candidate.countryCode == null,
|
||||
orElse: () => sameLanguage.first,
|
||||
);
|
||||
}
|
||||
return _fallback;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ void main() {
|
|||
serverName: instance.name,
|
||||
auth: AuthRepository(gateway, MemorySignInAddresses()),
|
||||
signedOut: signedOut,
|
||||
sourceCode: () async =>
|
||||
Uri.parse('https://code.halkyon.app/project-halkyon/halkyon'),
|
||||
sourceCodeUrl: () async =>
|
||||
'https://code.halkyon.app/project-halkyon/halkyon',
|
||||
openPerson: (person) async {
|
||||
opened.add(person);
|
||||
return TodoPerson(person);
|
||||
|
|
|
|||
Loading…
Reference in a new issue