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
11 changed files with 1001 additions and 834 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue