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
59 changed files with 1001 additions and 834 deletions
|
|
@ -10,7 +10,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
||||||
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
||||||
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of, with texts of its own (English, German).
|
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of, with texts of its own (English, German).
|
||||||
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (anonymous, GitHub, email codes), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (anonymous, GitHub, email codes), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
||||||
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, with texts that name the app.
|
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, the account section of an app's settings (`AccountTiles`), About with the instance's source link (`showAppAbout`), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
|
||||||
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
||||||
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files are to join Learn's. **`snap/`**: Learn's snap, built from the root, which its build needs whole. **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files are to join Learn's. **`snap/`**: Learn's snap, built from the root, which its build needs whole. **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
||||||
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's and Todo's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's and Todo's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -595,9 +595,6 @@
|
||||||
"instanceAddressInvalid": "Das ist keine Adresse.",
|
"instanceAddressInvalid": "Das ist keine Adresse.",
|
||||||
"instanceUnreachable": "Unter {name} antwortet keine Instanz von Halkyon Learn.",
|
"instanceUnreachable": "Unter {name} antwortet keine Instanz von Halkyon Learn.",
|
||||||
"instanceOpenFailed": "Die App auf {name} konnte nicht geöffnet werden.",
|
"instanceOpenFailed": "Die App auf {name} konnte nicht geöffnet werden.",
|
||||||
"settingsSwitchAccount": "Konto wechseln",
|
|
||||||
"settingsAccountAnonymous": "Anonym auf {instance}",
|
|
||||||
"settingsAccountSignedIn": "Angemeldet auf {instance}",
|
|
||||||
"accountAnonymous": "Anonym",
|
"accountAnonymous": "Anonym",
|
||||||
"accountSignedIn": "Angemeldet",
|
"accountSignedIn": "Angemeldet",
|
||||||
"accountsTitle": "Konten",
|
"accountsTitle": "Konten",
|
||||||
|
|
@ -607,16 +604,10 @@
|
||||||
"accountRemoveTitle": "Konto entfernen?",
|
"accountRemoveTitle": "Konto entfernen?",
|
||||||
"accountRemoveMessage": "Entfernt das Konto auf {name} von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf {name}: Melde dich dort wieder an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden.",
|
"accountRemoveMessage": "Entfernt das Konto auf {name} von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf {name}: Melde dich dort wieder an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden.",
|
||||||
"accountRemoveConfirm": "Entfernen",
|
"accountRemoveConfirm": "Entfernen",
|
||||||
"settingsSignOut": "Abmelden",
|
|
||||||
"settingsSignOutTitle": "Abmelden?",
|
|
||||||
"settingsSignOutAnonymousMessage": "Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden samt Fortschritt gelöscht.",
|
"settingsSignOutAnonymousMessage": "Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden samt Fortschritt gelöscht.",
|
||||||
"settingsDeleteAccount": "Konto löschen",
|
|
||||||
"settingsDeleteTitle": "Dein Konto löschen?",
|
|
||||||
"settingsDeleteMessage": "Alles, was du hast, also deine Lernsets, Karten und Wiederholungen, wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.",
|
"settingsDeleteMessage": "Alles, was du hast, also deine Lernsets, Karten und Wiederholungen, wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.",
|
||||||
"settingsAbout": "Über Halkyon Learn",
|
"settingsAbout": "Über Halkyon Learn",
|
||||||
"aboutLegalese": "Freie Software: Die App steht unter der GNU Affero General Public License, Version 3 oder jeder späteren Version, der Server unter der Server Side Public License, Version 1.",
|
"aboutLegalese": "Freie Software: Die App steht unter der GNU Affero General Public License, Version 3 oder jeder späteren Version, der Server unter der Server Side Public License, Version 1.",
|
||||||
"aboutSourceCode": "Der Quellcode dessen, was diese Instanz ausführt:",
|
|
||||||
"aboutSourceUnreachable": "Diese Instanz war nicht erreichbar, um nach ihrem Quellcode zu fragen.",
|
|
||||||
"settingsAppearance": "Darstellung",
|
"settingsAppearance": "Darstellung",
|
||||||
"settingsAppearanceDevice": "Gerätedesign",
|
"settingsAppearanceDevice": "Gerätedesign",
|
||||||
"settingsAppearanceLight": "Hell",
|
"settingsAppearanceLight": "Hell",
|
||||||
|
|
|
||||||
|
|
@ -677,11 +677,6 @@
|
||||||
"@instanceUnreachable": {"placeholders": {"name": {"type": "String"}}},
|
"@instanceUnreachable": {"placeholders": {"name": {"type": "String"}}},
|
||||||
"instanceOpenFailed": "Couldn't open the app on {name}.",
|
"instanceOpenFailed": "Couldn't open the app on {name}.",
|
||||||
"@instanceOpenFailed": {"placeholders": {"name": {"type": "String"}}},
|
"@instanceOpenFailed": {"placeholders": {"name": {"type": "String"}}},
|
||||||
"settingsSwitchAccount": "Switch account",
|
|
||||||
"settingsAccountAnonymous": "Anonymous on {instance}",
|
|
||||||
"@settingsAccountAnonymous": {"placeholders": {"instance": {"type": "String"}}},
|
|
||||||
"settingsAccountSignedIn": "Signed in on {instance}",
|
|
||||||
"@settingsAccountSignedIn": {"placeholders": {"instance": {"type": "String"}}},
|
|
||||||
"accountAnonymous": "Anonymous",
|
"accountAnonymous": "Anonymous",
|
||||||
"accountSignedIn": "Signed in",
|
"accountSignedIn": "Signed in",
|
||||||
"accountsTitle": "Accounts",
|
"accountsTitle": "Accounts",
|
||||||
|
|
@ -692,16 +687,10 @@
|
||||||
"accountRemoveMessage": "Removes the account on {name} from this device, with all the device holds of it. What it synced stays on {name}: sign in there again to have it. An anonymous account can't be signed into again.",
|
"accountRemoveMessage": "Removes the account on {name} from this device, with all the device holds of it. What it synced stays on {name}: sign in there again to have it. An anonymous account can't be signed into again.",
|
||||||
"@accountRemoveMessage": {"placeholders": {"name": {"type": "String"}}},
|
"@accountRemoveMessage": {"placeholders": {"name": {"type": "String"}}},
|
||||||
"accountRemoveConfirm": "Remove",
|
"accountRemoveConfirm": "Remove",
|
||||||
"settingsSignOut": "Sign out",
|
|
||||||
"settingsSignOutTitle": "Sign out?",
|
|
||||||
"settingsSignOutAnonymousMessage": "This account can't be signed into again, so signing out deletes it with its progress.",
|
"settingsSignOutAnonymousMessage": "This account can't be signed into again, so signing out deletes it with its progress.",
|
||||||
"settingsDeleteAccount": "Delete account",
|
|
||||||
"settingsDeleteTitle": "Delete your account?",
|
|
||||||
"settingsDeleteMessage": "Everything you have, your studysets, cards and reviews, is deleted on the server and on all your devices. This can't be undone.",
|
"settingsDeleteMessage": "Everything you have, your studysets, cards and reviews, is deleted on the server and on all your devices. This can't be undone.",
|
||||||
"settingsAbout": "About Halkyon Learn",
|
"settingsAbout": "About Halkyon Learn",
|
||||||
"aboutLegalese": "Free software: the app is under the GNU Affero General Public License, version 3 or any later version, the server under the Server Side Public License, version 1.",
|
"aboutLegalese": "Free software: the app is under the GNU Affero General Public License, version 3 or any later version, the server under the Server Side Public License, version 1.",
|
||||||
"aboutSourceCode": "The source of what this instance runs:",
|
|
||||||
"aboutSourceUnreachable": "Couldn't reach this instance to ask where its source is.",
|
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
"settingsAppearanceDevice": "Device theme",
|
"settingsAppearanceDevice": "Device theme",
|
||||||
"settingsAppearanceLight": "Light",
|
"settingsAppearanceLight": "Light",
|
||||||
|
|
|
||||||
|
|
@ -3562,24 +3562,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Couldn\'t open the app on {name}.'**
|
/// **'Couldn\'t open the app on {name}.'**
|
||||||
String instanceOpenFailed(String name);
|
String instanceOpenFailed(String name);
|
||||||
|
|
||||||
/// No description provided for @settingsSwitchAccount.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Switch account'**
|
|
||||||
String get settingsSwitchAccount;
|
|
||||||
|
|
||||||
/// No description provided for @settingsAccountAnonymous.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Anonymous on {instance}'**
|
|
||||||
String settingsAccountAnonymous(String instance);
|
|
||||||
|
|
||||||
/// No description provided for @settingsAccountSignedIn.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Signed in on {instance}'**
|
|
||||||
String settingsAccountSignedIn(String instance);
|
|
||||||
|
|
||||||
/// No description provided for @accountAnonymous.
|
/// No description provided for @accountAnonymous.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3634,36 +3616,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Remove'**
|
/// **'Remove'**
|
||||||
String get accountRemoveConfirm;
|
String get accountRemoveConfirm;
|
||||||
|
|
||||||
/// No description provided for @settingsSignOut.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Sign out'**
|
|
||||||
String get settingsSignOut;
|
|
||||||
|
|
||||||
/// No description provided for @settingsSignOutTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Sign out?'**
|
|
||||||
String get settingsSignOutTitle;
|
|
||||||
|
|
||||||
/// No description provided for @settingsSignOutAnonymousMessage.
|
/// No description provided for @settingsSignOutAnonymousMessage.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'This account can\'t be signed into again, so signing out deletes it with its progress.'**
|
/// **'This account can\'t be signed into again, so signing out deletes it with its progress.'**
|
||||||
String get settingsSignOutAnonymousMessage;
|
String get settingsSignOutAnonymousMessage;
|
||||||
|
|
||||||
/// No description provided for @settingsDeleteAccount.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Delete account'**
|
|
||||||
String get settingsDeleteAccount;
|
|
||||||
|
|
||||||
/// No description provided for @settingsDeleteTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Delete your account?'**
|
|
||||||
String get settingsDeleteTitle;
|
|
||||||
|
|
||||||
/// No description provided for @settingsDeleteMessage.
|
/// No description provided for @settingsDeleteMessage.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3682,18 +3640,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Free software: the app is under the GNU Affero General Public License, version 3 or any later version, the server under the Server Side Public License, version 1.'**
|
/// **'Free software: the app is under the GNU Affero General Public License, version 3 or any later version, the server under the Server Side Public License, version 1.'**
|
||||||
String get aboutLegalese;
|
String get aboutLegalese;
|
||||||
|
|
||||||
/// No description provided for @aboutSourceCode.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'The source of what this instance runs:'**
|
|
||||||
String get aboutSourceCode;
|
|
||||||
|
|
||||||
/// No description provided for @aboutSourceUnreachable.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Couldn\'t reach this instance to ask where its source is.'**
|
|
||||||
String get aboutSourceUnreachable;
|
|
||||||
|
|
||||||
/// No description provided for @settingsAppearance.
|
/// No description provided for @settingsAppearance.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -2155,19 +2155,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return 'Die App auf $name konnte nicht geöffnet werden.';
|
return 'Die App auf $name konnte nicht geöffnet werden.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsSwitchAccount => 'Konto wechseln';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String settingsAccountAnonymous(String instance) {
|
|
||||||
return 'Anonym auf $instance';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String settingsAccountSignedIn(String instance) {
|
|
||||||
return 'Angemeldet auf $instance';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get accountAnonymous => 'Anonym';
|
String get accountAnonymous => 'Anonym';
|
||||||
|
|
||||||
|
|
@ -2198,22 +2185,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get accountRemoveConfirm => 'Entfernen';
|
String get accountRemoveConfirm => 'Entfernen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsSignOut => 'Abmelden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsSignOutTitle => 'Abmelden?';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsSignOutAnonymousMessage =>
|
String get settingsSignOutAnonymousMessage =>
|
||||||
'Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden samt Fortschritt gelöscht.';
|
'Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden samt Fortschritt gelöscht.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsDeleteAccount => 'Konto löschen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsDeleteTitle => 'Dein Konto löschen?';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsDeleteMessage =>
|
String get settingsDeleteMessage =>
|
||||||
'Alles, was du hast, also deine Lernsets, Karten und Wiederholungen, wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.';
|
'Alles, was du hast, also deine Lernsets, Karten und Wiederholungen, wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.';
|
||||||
|
|
@ -2225,14 +2200,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get aboutLegalese =>
|
String get aboutLegalese =>
|
||||||
'Freie Software: Die App steht unter der GNU Affero General Public License, Version 3 oder jeder späteren Version, der Server unter der Server Side Public License, Version 1.';
|
'Freie Software: Die App steht unter der GNU Affero General Public License, Version 3 oder jeder späteren Version, der Server unter der Server Side Public License, Version 1.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get aboutSourceCode =>
|
|
||||||
'Der Quellcode dessen, was diese Instanz ausführt:';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get aboutSourceUnreachable =>
|
|
||||||
'Diese Instanz war nicht erreichbar, um nach ihrem Quellcode zu fragen.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsAppearance => 'Darstellung';
|
String get settingsAppearance => 'Darstellung';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2135,19 +2135,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return 'Couldn\'t open the app on $name.';
|
return 'Couldn\'t open the app on $name.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsSwitchAccount => 'Switch account';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String settingsAccountAnonymous(String instance) {
|
|
||||||
return 'Anonymous on $instance';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String settingsAccountSignedIn(String instance) {
|
|
||||||
return 'Signed in on $instance';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get accountAnonymous => 'Anonymous';
|
String get accountAnonymous => 'Anonymous';
|
||||||
|
|
||||||
|
|
@ -2178,22 +2165,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get accountRemoveConfirm => 'Remove';
|
String get accountRemoveConfirm => 'Remove';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsSignOut => 'Sign out';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsSignOutTitle => 'Sign out?';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsSignOutAnonymousMessage =>
|
String get settingsSignOutAnonymousMessage =>
|
||||||
'This account can\'t be signed into again, so signing out deletes it with its progress.';
|
'This account can\'t be signed into again, so signing out deletes it with its progress.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsDeleteAccount => 'Delete account';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsDeleteTitle => 'Delete your account?';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsDeleteMessage =>
|
String get settingsDeleteMessage =>
|
||||||
'Everything you have, your studysets, cards and reviews, is deleted on the server and on all your devices. This can\'t be undone.';
|
'Everything you have, your studysets, cards and reviews, is deleted on the server and on all your devices. This can\'t be undone.';
|
||||||
|
|
@ -2205,13 +2180,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get aboutLegalese =>
|
String get aboutLegalese =>
|
||||||
'Free software: the app is under the GNU Affero General Public License, version 3 or any later version, the server under the Server Side Public License, version 1.';
|
'Free software: the app is under the GNU Affero General Public License, version 3 or any later version, the server under the Server Side Public License, version 1.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get aboutSourceCode => 'The source of what this instance runs:';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get aboutSourceUnreachable =>
|
|
||||||
'Couldn\'t reach this instance to ask where its source is.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsAppearance => 'Appearance';
|
String get settingsAppearance => 'Appearance';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/app/learn_frame.dart';
|
import 'package:halkyon_learn_flutter/src/app/learn_frame.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/app/learn_sessions.dart';
|
import 'package:halkyon_learn_flutter/src/app/learn_sessions.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/bloc/app_bloc_observer.dart';
|
import 'package:halkyon_learn_flutter/src/core/bloc/app_bloc_observer.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/country/country_names.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/widget/device_settings_scope.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/widget/device_settings_scope.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/theme/cjk_fonts.dart';
|
import 'package:halkyon_learn_flutter/src/core/theme/cjk_fonts.dart';
|
||||||
|
|
@ -46,7 +45,6 @@ Future<void> main() async {
|
||||||
// Only an instance of Halkyon Learn: one of another Halkyon app's
|
// Only an instance of Halkyon Learn: one of another Halkyon app's
|
||||||
// answers too, but holds nothing of this app's.
|
// answers too, but holds nothing of this app's.
|
||||||
checker: ServerpodInstanceChecker(app: 'learn'),
|
checker: ServerpodInstanceChecker(app: 'learn'),
|
||||||
countryNames: AssetCountryNames(),
|
|
||||||
open: (instance) => Dependencies.create(instance, device),
|
open: (instance) => Dependencies.create(instance, device),
|
||||||
readLook: () async => (await device.deviceSettingsStore.read()).look,
|
readLook: () async => (await device.deviceSettingsStore.read()).look,
|
||||||
app: const LearnSessions(),
|
app: const LearnSessions(),
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
|
||||||
|
|
||||||
/// Countries' names as halkyon_app asks for them ([CountryNames]): the world
|
|
||||||
/// map's, Unicode CLDR's (`assets/maps/world_countries.geojson`, whose notice
|
|
||||||
/// the licence list holds), read when first asked for.
|
|
||||||
class AssetCountryNames([final AssetBundle? _bundle]) implements CountryNames {
|
|
||||||
/// The world map, which the map questions read too.
|
|
||||||
static const asset = 'assets/maps/world_countries.geojson';
|
|
||||||
|
|
||||||
// The names themselves, not the future reading them (AGENTS.md, Testing).
|
|
||||||
Map<String, Map<String, String>>? _names;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> nameOf(String code, String language) async {
|
|
||||||
final names = _names ??= await _read();
|
|
||||||
return countryNameIn(names, code, language);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, Map<String, String>>> _read() async {
|
|
||||||
final geojson = await (_bundle ?? rootBundle).loadString(asset);
|
|
||||||
// Some 2 MB of outlines around the names: read where no frame waits.
|
|
||||||
return await Isolate.run(() => namesIn(geojson));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The names of the countries in [geojson], a map's features, by code;
|
|
||||||
/// those without a code (Natural Earth's `-99`) left out.
|
|
||||||
Map<String, Map<String, String>> namesIn(String geojson) {
|
|
||||||
final features =
|
|
||||||
(jsonDecode(geojson) as Map<String, Object?>)['features']!
|
|
||||||
as List<Object?>;
|
|
||||||
return {
|
|
||||||
for (final feature in features.cast<Map<String, Object?>>())
|
|
||||||
if (feature['properties']
|
|
||||||
case {
|
|
||||||
'code': final String code,
|
|
||||||
'names': final Map<String, Object?> names,
|
|
||||||
}
|
|
||||||
when RegExp(r'^[A-Z]{2}$').hasMatch(code))
|
|
||||||
code: names.cast<String, String>(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -18,8 +18,6 @@ import 'package:halkyon_learn_flutter/src/core/files/file_saver.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
|
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
|
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/serverpod_about_gateway.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package_reader.dart';
|
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package_reader.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
|
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_hand_off.dart';
|
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_hand_off.dart';
|
||||||
|
|
@ -109,8 +107,9 @@ class Dependencies({
|
||||||
/// Whether the person here signed out, so the app asks how to start.
|
/// Whether the person here signed out, so the app asks how to start.
|
||||||
@override required final SignedOutMark signedOut,
|
@override required final SignedOutMark signedOut,
|
||||||
|
|
||||||
/// What the instance says about itself, such as where its source is.
|
/// Where the source of what the instance runs is, as it says
|
||||||
required final AboutRepository about,
|
/// ([sourceCodeUrl]).
|
||||||
|
required final Future<String> Function() _sourceCodeUrl,
|
||||||
|
|
||||||
/// The dictionaries the instance holds.
|
/// The dictionaries the instance holds.
|
||||||
required final DictionaryRepository dictionaries,
|
required final DictionaryRepository dictionaries,
|
||||||
|
|
@ -192,6 +191,9 @@ class Dependencies({
|
||||||
@override
|
@override
|
||||||
Future<PersonDependencies> openPerson(SignedIn person) => _openPerson(person);
|
Future<PersonDependencies> openPerson(SignedIn person) => _openPerson(person);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> sourceCodeUrl() => _sourceCodeUrl();
|
||||||
|
|
||||||
/// Builds the production graph on [instance], around one Serverpod
|
/// Builds the production graph on [instance], around one Serverpod
|
||||||
/// client of its own, with what [device] shares across instances. Its
|
/// client of its own, with what [device] shares across instances. Its
|
||||||
/// session and its people's folders are the instance's own
|
/// session and its people's folders are the instance's own
|
||||||
|
|
@ -245,7 +247,7 @@ class Dependencies({
|
||||||
FileSignInAddresses(device.storage.peopleOf(instance)),
|
FileSignInAddresses(device.storage.peopleOf(instance)),
|
||||||
),
|
),
|
||||||
signedOut: FileSignedOutMark(device.storage.peopleOf(instance)),
|
signedOut: FileSignedOutMark(device.storage.peopleOf(instance)),
|
||||||
about: AboutRepository(ServerpodAboutGateway(client)),
|
sourceCodeUrl: () async => (await client.instance.info()).sourceCodeUrl,
|
||||||
dictionaries: DictionaryRepository(
|
dictionaries: DictionaryRepository(
|
||||||
ServerpodDictionaryGateway(client),
|
ServerpodDictionaryGateway(client),
|
||||||
deviceDictionaries,
|
deviceDictionaries,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ const appLanguages = [
|
||||||
];
|
];
|
||||||
|
|
||||||
/// The English the app speaks wherever it can't speak the language asked for:
|
/// The English the app speaks wherever it can't speak the language asked for:
|
||||||
/// British English, which `app_en.arb` holds.
|
/// British English, which `app_en.arb` holds (as halkyon_app's
|
||||||
|
/// `resolveAppLocale` falls back to).
|
||||||
const _fallback = Locale('en', 'GB');
|
const _fallback = Locale('en', 'GB');
|
||||||
|
|
||||||
/// The locale of the BCP 47 [tag], as `en-GB`.
|
/// The locale of the BCP 47 [tag], as `en-GB`.
|
||||||
|
|
@ -40,37 +41,3 @@ Locale localeOf(String tag) => switch (tag.split('-')) {
|
||||||
[final language, ...] => Locale(language),
|
[final language, ...] => Locale(language),
|
||||||
_ => _fallback,
|
_ => _fallback,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Which of the [supported] locales the app speaks, for a device that prefers
|
|
||||||
/// the [preferred] ones in this order (or a person who chose one).
|
|
||||||
///
|
|
||||||
/// The first preferred language the app speaks wins: in its country's
|
|
||||||
/// variant where there is one, otherwise as the language alone, and English
|
|
||||||
/// of any other country as British English. Unlike Flutter's own resolution,
|
|
||||||
/// a country alone never picks a language (Spanish in the United States
|
|
||||||
/// isn't American English), and where none matches, the app speaks British
|
|
||||||
/// English rather than the first supported locale.
|
|
||||||
Locale resolveAppLocale(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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
|
|
||||||
|
|
||||||
/// Finds where the source of what the instance runs is, for the About dialog.
|
|
||||||
class AboutBloc(final AboutRepository _about)
|
|
||||||
extends Bloc<AboutEvent, AboutState> {
|
|
||||||
this : super(const AboutStateLoading()) {
|
|
||||||
on<AboutEventStarted>(_onStarted);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onStarted(
|
|
||||||
AboutEventStarted event,
|
|
||||||
Emitter<AboutState> emit,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
emit(AboutStateLoaded(await _about.sourceCodeUrl()));
|
|
||||||
} on Exception catch (error, st) {
|
|
||||||
addError(error, st);
|
|
||||||
emit(const AboutStateUnreachable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What happens to the About dialog.
|
|
||||||
sealed class const AboutEvent() with Equatable {
|
|
||||||
@override
|
|
||||||
List<Object?> get props => const [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The About dialog opens.
|
|
||||||
final class const AboutEventStarted() extends AboutEvent;
|
|
||||||
|
|
||||||
/// What the About dialog knows about the instance.
|
|
||||||
sealed class const AboutState() with Equatable {
|
|
||||||
@override
|
|
||||||
List<Object?> get props => const [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asking the instance.
|
|
||||||
final class const AboutStateLoading() extends AboutState;
|
|
||||||
|
|
||||||
/// Where the source of what the instance runs is.
|
|
||||||
final class const AboutStateLoaded(
|
|
||||||
/// Its address.
|
|
||||||
final String sourceCodeUrl,
|
|
||||||
) extends AboutState {
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [sourceCodeUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The instance couldn't be reached.
|
|
||||||
final class const AboutStateUnreachable() extends AboutState;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
/// The boundary to what the instance says about itself. Tests replace it with
|
|
||||||
/// a fake.
|
|
||||||
abstract interface class AboutGateway() {
|
|
||||||
/// Where the source of what the instance runs is.
|
|
||||||
Future<String> sourceCode();
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/about_gateway.dart';
|
|
||||||
|
|
||||||
/// What the instance says about itself, as the About dialog shows it.
|
|
||||||
class AboutRepository(final AboutGateway _gateway) {
|
|
||||||
/// Where the source of what the instance runs is: both licences require
|
|
||||||
/// offering it to the people who use the instance.
|
|
||||||
Future<String> sourceCodeUrl() => _gateway.sourceCode();
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import 'package:halkyon_learn_client/halkyon_learn_client.dart' show Client;
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/about_gateway.dart';
|
|
||||||
|
|
||||||
/// [AboutGateway] asking the instance through the Serverpod client.
|
|
||||||
class ServerpodAboutGateway(final Client _client) implements AboutGateway {
|
|
||||||
@override
|
|
||||||
Future<String> sourceCode() async =>
|
|
||||||
(await _client.instance.info()).sourceCodeUrl;
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +1,19 @@
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/bloc/about_bloc.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// Shows what Halkyon Learn is under which licences, where the source of
|
/// Shows what Halkyon Learn is under which licences, where the source of
|
||||||
/// what this instance runs is, and whose work its dictionaries and the data
|
/// what this instance runs is, and whose work its dictionaries and the data
|
||||||
/// the app comes with are, as their licences ask; its "View licenses" lists
|
/// the app comes with are, as their licences ask; halkyon_app's About
|
||||||
|
/// ([showAppAbout]) with Learn's own parts, whose "View licenses" lists
|
||||||
/// every package's notice, which the BSD and MIT packages require in a
|
/// every package's notice, which the BSD and MIT packages require in a
|
||||||
/// distributed app, the fonts' and KanjiVG's.
|
/// distributed app, the fonts' and KanjiVG's.
|
||||||
void showAbout(BuildContext context, AboutRepository about) => showAboutDialog(
|
void showAbout(BuildContext context) => showAppAbout(
|
||||||
context: context,
|
context,
|
||||||
applicationName: AppLocalizations.of(context).appTitle,
|
legalese: AppLocalizations.of(context).aboutLegalese,
|
||||||
applicationLegalese: AppLocalizations.of(context).aboutLegalese,
|
children: const [_Dictionaries(), _AppData()],
|
||||||
children: [
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) => AboutBloc(about)..add(const AboutEventStarted()),
|
|
||||||
child: const _SourceCode(),
|
|
||||||
),
|
|
||||||
const _Dictionaries(),
|
|
||||||
const _AppData(),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Whose work the data the app comes with is: KanjiVG's strokes, whose
|
/// Whose work the data the app comes with is: KanjiVG's strokes, whose
|
||||||
|
|
@ -105,26 +96,3 @@ class _DictionariesState() extends State<_Dictionaries> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class const _SourceCode() extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 16),
|
|
||||||
child: BlocBuilder<AboutBloc, AboutState>(
|
|
||||||
builder: (context, state) => switch (state) {
|
|
||||||
AboutStateLoading() => const SizedBox.shrink(),
|
|
||||||
AboutStateLoaded(:final sourceCodeUrl) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(AppLocalizations.of(context).aboutSourceCode),
|
|
||||||
SelectableText(sourceCodeUrl),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
AboutStateUnreachable() => Text(
|
|
||||||
AppLocalizations.of(context).aboutSourceUnreachable,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,6 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final identified = switch (context.watch<SessionBloc>().state) {
|
|
||||||
SessionStateActive(:final person) => person.isIdentified,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
final instance =
|
|
||||||
InstanceScope.maybeOf(context)?.choice.current?.name ??
|
|
||||||
Dependencies.of(context).serverName;
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return TopGradient(
|
return TopGradient(
|
||||||
color: Brand.primary,
|
color: Brand.primary,
|
||||||
|
|
@ -78,45 +71,14 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Which account, on which instance (decided
|
// Which account, on which instance (decided
|
||||||
// 2026-10-16).
|
// 2026-10-16), switching, signing out and deleting it:
|
||||||
ListTile(
|
// halkyon_app's, in Learn's words where only Learn can
|
||||||
leading: Icon(
|
// say it.
|
||||||
identified
|
AccountTiles(
|
||||||
? LucideIcons.userCheck
|
anonymousNote: l10n.profileAnonymous,
|
||||||
: LucideIcons.userRound,
|
signOutAnonymousMessage:
|
||||||
),
|
l10n.settingsSignOutAnonymousMessage,
|
||||||
title: Text(
|
deleteMessage: l10n.settingsDeleteMessage,
|
||||||
identified
|
|
||||||
? l10n.settingsAccountSignedIn(instance)
|
|
||||||
: l10n.settingsAccountAnonymous(instance),
|
|
||||||
),
|
|
||||||
subtitle: identified
|
|
||||||
? null
|
|
||||||
: Text(l10n.profileAnonymous),
|
|
||||||
),
|
|
||||||
// The accounts on this device, one per instance.
|
|
||||||
if (InstanceScope.maybeOf(context) case final scope?)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(LucideIcons.users),
|
|
||||||
title: Text(l10n.settingsSwitchAccount),
|
|
||||||
subtitle: Text(scope.choice.current?.name ?? ''),
|
|
||||||
onTap: () => showAccountSwitcher(context),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(LucideIcons.logOut),
|
|
||||||
title: Text(l10n.settingsSignOut),
|
|
||||||
onTap: () => _signOut(context),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
LucideIcons.trash2,
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
l10n.settingsDeleteAccount,
|
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
|
||||||
),
|
|
||||||
onTap: () => _delete(context),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -127,8 +89,7 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(LucideIcons.info),
|
leading: const Icon(LucideIcons.info),
|
||||||
title: Text(l10n.settingsAbout),
|
title: Text(l10n.settingsAbout),
|
||||||
onTap: () =>
|
onTap: () => showAbout(context),
|
||||||
showAbout(context, Dependencies.of(context).about),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
@ -163,41 +124,6 @@ class const _Version() extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signs out; an anonymous account can't be signed into again, so signing out
|
|
||||||
/// of one deletes it, and asks first. The app then starts anew.
|
|
||||||
Future<void> _signOut(BuildContext context) async {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final session = context.read<SessionBloc>();
|
|
||||||
final isIdentified = switch (session.state) {
|
|
||||||
SessionStateActive(:final person) => person.isIdentified,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if (!isIdentified &&
|
|
||||||
!await confirmDestructiveAction(
|
|
||||||
context,
|
|
||||||
title: l10n.settingsSignOutTitle,
|
|
||||||
message: l10n.settingsSignOutAnonymousMessage,
|
|
||||||
confirmLabel: l10n.settingsSignOut,
|
|
||||||
)) {
|
|
||||||
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.settingsDeleteTitle,
|
|
||||||
message: l10n.settingsDeleteMessage,
|
|
||||||
confirmLabel: l10n.settingsDeleteAccount,
|
|
||||||
)) {
|
|
||||||
session.add(const SessionEventDeleteRequested());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the person shares of their learning: whether it counts in Explore's
|
/// What the person shares of their learning: whether it counts in Explore's
|
||||||
/// "learn this here", which nobody does unless they choose to.
|
/// "learn this here", which nobody does unless they choose to.
|
||||||
class const _PrivacyCard() extends StatelessWidget {
|
class const _PrivacyCard() extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/core/country/country_names.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test("a map's countries are named by their code, in each language it "
|
|
||||||
'has', () {
|
|
||||||
final names = namesIn('''
|
|
||||||
{"type": "FeatureCollection", "features": [
|
|
||||||
{"type": "Feature", "properties": {"code": "CH",
|
|
||||||
"names": {"de": "Schweiz", "en": "Switzerland"}}, "geometry": null},
|
|
||||||
{"type": "Feature", "properties": {"code": "-99",
|
|
||||||
"names": {"en": "Somewhere without a code"}}, "geometry": null}
|
|
||||||
]}
|
|
||||||
''');
|
|
||||||
|
|
||||||
expect(names, {
|
|
||||||
'CH': {'de': 'Schweiz', 'en': 'Switzerland'},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets("the world map's names are read from the app's own", (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
final name = await tester.runAsync(
|
|
||||||
() => AssetCountryNames().nameOf('CH', 'de'),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(name, 'Schweiz');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +1,12 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('the language the app speaks', () {
|
// How the language is resolved is halkyon_app's (`resolveAppLocale`),
|
||||||
Locale resolved(List<Locale>? preferred) =>
|
// tested there; Learn's own languages are tested here.
|
||||||
resolveAppLocale(preferred, AppLocalizations.supportedLocales);
|
|
||||||
|
|
||||||
test('is the one asked for, where the app speaks it', () {
|
|
||||||
expect(resolved(const [Locale('en', 'US')]), const Locale('en', 'US'));
|
|
||||||
expect(resolved(const [Locale('en', 'GB')]), const Locale('en', 'GB'));
|
|
||||||
expect(resolved(const [Locale('de', 'DE')]), const Locale('de'));
|
|
||||||
expect(resolved(const [Locale('de')]), const Locale('de'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('is British English for any other English', () {
|
|
||||||
expect(resolved(const [Locale('en')]), const Locale('en', 'GB'));
|
|
||||||
expect(resolved(const [Locale('en', 'AU')]), const Locale('en', 'GB'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('follows the next language the device prefers', () {
|
|
||||||
expect(
|
|
||||||
resolved(const [Locale('fr', 'FR'), Locale('de', 'AT')]),
|
|
||||||
const Locale('de'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('falls back to British English', () {
|
|
||||||
expect(resolved(const [Locale('fr', 'FR')]), const Locale('en', 'GB'));
|
|
||||||
expect(resolved(const []), const Locale('en', 'GB'));
|
|
||||||
expect(resolved(null), const Locale('en', 'GB'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('never picks a language by country alone', () {
|
|
||||||
// Spanish in the United States isn't American English.
|
|
||||||
expect(resolved(const [Locale('es', 'US')]), const Locale('en', 'GB'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('every language offered is one the app speaks', () {
|
test('every language offered is one the app speaks', () {
|
||||||
for (final language in appLanguages) {
|
for (final language in appLanguages) {
|
||||||
final locale = localeOf(language.tag);
|
final locale = localeOf(language.tag);
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,10 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/bloc/about_bloc.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
|
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
import '../../support/fake_about_gateway.dart';
|
|
||||||
import '../../support/pump_app.dart';
|
import '../../support/pump_app.dart';
|
||||||
import '../../support/test_dependencies.dart';
|
import '../../support/test_dependencies.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('AboutBloc', () {
|
|
||||||
test('finds where the instance says its source is', () async {
|
|
||||||
final bloc = AboutBloc(AboutRepository(FakeAboutGateway()))
|
|
||||||
..add(const AboutEventStarted());
|
|
||||||
addTearDown(bloc.close);
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
bloc.stream,
|
|
||||||
emits(const AboutStateLoaded('https://example.org/halkyon-learn')),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('says so when the instance cannot be reached', () async {
|
|
||||||
final gateway = FakeAboutGateway()
|
|
||||||
..unreachable = Exception('No connection');
|
|
||||||
final bloc = AboutBloc(AboutRepository(gateway))
|
|
||||||
..add(const AboutEventStarted());
|
|
||||||
addTearDown(bloc.close);
|
|
||||||
|
|
||||||
await expectLater(bloc.stream, emits(const AboutStateUnreachable()));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('profile → settings → about shows the licences and the source', (
|
testWidgets('profile → settings → about shows the licences and the source', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue