feat(learn): Settings' dictionaries on this device, one row a language
The developer's review of 4 Nov 2026. "Japanese on this device" becomes "Dictionaries on this device", with one row a language: its name at the start, what the device holds or that the download failed beneath it, and its action at the end (Download with its size, the progress, Remove), built as a list of rows so Korean's and Chinese's line up beneath later. Japanese's is the only one now. The help speaks of a language's dictionaries in general, then of what Japanese's hold (MeCab's, JMdict). Whats-New: Settings lists the dictionaries on this device by language, each with its own Download or Remove. Whats-New-De: Die Einstellungen listen die Wörterbücher auf diesem Gerät nach Sprache, jede mit eigenem Herunterladen oder Entfernen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
12bc31b982
commit
a7638e5e54
8 changed files with 106 additions and 60 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1964,8 +1964,8 @@
|
||||||
"deviceDictionaryPreparing": "Wird eingerichtet…",
|
"deviceDictionaryPreparing": "Wird eingerichtet…",
|
||||||
"deviceDictionaryReady": "Wörter werden jetzt auf diesem Gerät nachgeschlagen.",
|
"deviceDictionaryReady": "Wörter werden jetzt auf diesem Gerät nachgeschlagen.",
|
||||||
"deviceDictionaryFailed": "Die Wörterbücher konnten nicht heruntergeladen werden. Versuch es in den Einstellungen noch einmal.",
|
"deviceDictionaryFailed": "Die Wörterbücher konnten nicht heruntergeladen werden. Versuch es in den Einstellungen noch einmal.",
|
||||||
"settingsDeviceDictionaryTitle": "Japanisch auf diesem Gerät",
|
"settingsDeviceDictionaryTitle": "Wörterbücher auf diesem Gerät",
|
||||||
"settingsDeviceDictionaryAbout": "Mit den japanischen Wörterbüchern auf diesem Gerät werden die Zeilen von Videos und Liedern hier in Wörter zerlegt, wie Jisho.org es tut, und ihr Text verlässt es nicht. Ohne sie fragt die App die Instanz, die antwortet und nichts speichert.",
|
"settingsDeviceDictionaryAbout": "Die Wörterbücher einer Sprache, auf dieses Gerät geladen, schlagen Wörter hier nach, und der Text verlässt es nicht. Ohne sie fragt die App die Instanz, die antwortet und nichts speichert. Die japanischen sind das von MeCab, das die Zeilen von Videos und Liedern in Wörter zerlegt, wie Jisho.org es tut, und JMdict, mit den Lesungen und Bedeutungen jedes Worts.",
|
||||||
"settingsDeviceDictionaryHeld": "Auf diesem Gerät: {size}",
|
"settingsDeviceDictionaryHeld": "Auf diesem Gerät: {size}",
|
||||||
"@settingsDeviceDictionaryHeld": {"placeholders": {"size": {"type": "String"}}},
|
"@settingsDeviceDictionaryHeld": {"placeholders": {"size": {"type": "String"}}},
|
||||||
"settingsDeviceDictionaryDownload": "Herunterladen ({size})",
|
"settingsDeviceDictionaryDownload": "Herunterladen ({size})",
|
||||||
|
|
|
||||||
|
|
@ -2361,8 +2361,8 @@
|
||||||
"deviceDictionaryPreparing": "Setting them up…",
|
"deviceDictionaryPreparing": "Setting them up…",
|
||||||
"deviceDictionaryReady": "Words are now looked up on this device.",
|
"deviceDictionaryReady": "Words are now looked up on this device.",
|
||||||
"deviceDictionaryFailed": "Couldn't download the dictionaries. Try again in Settings.",
|
"deviceDictionaryFailed": "Couldn't download the dictionaries. Try again in Settings.",
|
||||||
"settingsDeviceDictionaryTitle": "Japanese on this device",
|
"settingsDeviceDictionaryTitle": "Dictionaries on this device",
|
||||||
"settingsDeviceDictionaryAbout": "With Japanese's dictionaries on this device, the lines of videos and songs are cut into words here, as Jisho.org does, and their text never leaves it. Without them, the app asks the instance, which answers and keeps nothing.",
|
"settingsDeviceDictionaryAbout": "A language's dictionaries, downloaded to this device, look words up here, and the text never leaves it. Without them, the app asks the instance, which answers and keeps nothing. Japanese's are MeCab's, which cuts the lines of videos and songs into words as Jisho.org does, and JMdict, with each word's readings and meanings.",
|
||||||
"settingsDeviceDictionaryHeld": "On this device: {size}",
|
"settingsDeviceDictionaryHeld": "On this device: {size}",
|
||||||
"@settingsDeviceDictionaryHeld": {"placeholders": {"size": {"type": "String"}}},
|
"@settingsDeviceDictionaryHeld": {"placeholders": {"size": {"type": "String"}}},
|
||||||
"settingsDeviceDictionaryDownload": "Download ({size})",
|
"settingsDeviceDictionaryDownload": "Download ({size})",
|
||||||
|
|
|
||||||
|
|
@ -11609,13 +11609,13 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @settingsDeviceDictionaryTitle.
|
/// No description provided for @settingsDeviceDictionaryTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Japanese on this device'**
|
/// **'Dictionaries on this device'**
|
||||||
String get settingsDeviceDictionaryTitle;
|
String get settingsDeviceDictionaryTitle;
|
||||||
|
|
||||||
/// No description provided for @settingsDeviceDictionaryAbout.
|
/// No description provided for @settingsDeviceDictionaryAbout.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'With Japanese\'s dictionaries on this device, the lines of videos and songs are cut into words here, as Jisho.org does, and their text never leaves it. Without them, the app asks the instance, which answers and keeps nothing.'**
|
/// **'A language\'s dictionaries, downloaded to this device, look words up here, and the text never leaves it. Without them, the app asks the instance, which answers and keeps nothing. Japanese\'s are MeCab\'s, which cuts the lines of videos and songs into words as Jisho.org does, and JMdict, with each word\'s readings and meanings.'**
|
||||||
String get settingsDeviceDictionaryAbout;
|
String get settingsDeviceDictionaryAbout;
|
||||||
|
|
||||||
/// No description provided for @settingsDeviceDictionaryHeld.
|
/// No description provided for @settingsDeviceDictionaryHeld.
|
||||||
|
|
|
||||||
|
|
@ -7474,11 +7474,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Die Wörterbücher konnten nicht heruntergeladen werden. Versuch es in den Einstellungen noch einmal.';
|
'Die Wörterbücher konnten nicht heruntergeladen werden. Versuch es in den Einstellungen noch einmal.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsDeviceDictionaryTitle => 'Japanisch auf diesem Gerät';
|
String get settingsDeviceDictionaryTitle => 'Wörterbücher auf diesem Gerät';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsDeviceDictionaryAbout =>
|
String get settingsDeviceDictionaryAbout =>
|
||||||
'Mit den japanischen Wörterbüchern auf diesem Gerät werden die Zeilen von Videos und Liedern hier in Wörter zerlegt, wie Jisho.org es tut, und ihr Text verlässt es nicht. Ohne sie fragt die App die Instanz, die antwortet und nichts speichert.';
|
'Die Wörterbücher einer Sprache, auf dieses Gerät geladen, schlagen Wörter hier nach, und der Text verlässt es nicht. Ohne sie fragt die App die Instanz, die antwortet und nichts speichert. Die japanischen sind das von MeCab, das die Zeilen von Videos und Liedern in Wörter zerlegt, wie Jisho.org es tut, und JMdict, mit den Lesungen und Bedeutungen jedes Worts.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settingsDeviceDictionaryHeld(String size) {
|
String settingsDeviceDictionaryHeld(String size) {
|
||||||
|
|
|
||||||
|
|
@ -7408,11 +7408,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
'Couldn\'t download the dictionaries. Try again in Settings.';
|
'Couldn\'t download the dictionaries. Try again in Settings.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsDeviceDictionaryTitle => 'Japanese on this device';
|
String get settingsDeviceDictionaryTitle => 'Dictionaries on this device';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsDeviceDictionaryAbout =>
|
String get settingsDeviceDictionaryAbout =>
|
||||||
'With Japanese\'s dictionaries on this device, the lines of videos and songs are cut into words here, as Jisho.org does, and their text never leaves it. Without them, the app asks the instance, which answers and keeps nothing.';
|
'A language\'s dictionaries, downloaded to this device, look words up here, and the text never leaves it. Without them, the app asks the instance, which answers and keeps nothing. Japanese\'s are MeCab\'s, which cuts the lines of videos and songs into words as Jisho.org does, and JMdict, with each word\'s readings and meanings.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settingsDeviceDictionaryHeld(String size) {
|
String settingsDeviceDictionaryHeld(String size) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.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';
|
||||||
|
|
@ -158,9 +160,11 @@ class const DeviceDictionaryProgressView({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings' card for Japanese's dictionaries on this device: what they are
|
/// Settings' card for the dictionaries on this device: what they are for,
|
||||||
/// for, and downloading or removing them. Shown where the instance offers
|
/// and one row a language, its name at the start and downloading, the
|
||||||
/// them or the device holds them.
|
/// download's progress or removing at the end, so the languages to come
|
||||||
|
/// line up beneath (the developer's review of 4 Nov 2026). Japanese's alone
|
||||||
|
/// for now. Shown where the instance offers them or the device holds them.
|
||||||
class const DeviceDictionarySettingsCard({super.key}) extends StatelessWidget {
|
class const DeviceDictionarySettingsCard({super.key}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) =>
|
Widget build(BuildContext context) =>
|
||||||
|
|
@ -174,11 +178,12 @@ class const _SettingsCard() extends StatelessWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final view = context.watch<DeviceDictionaryCubit>().state;
|
final view = context.watch<DeviceDictionaryCubit>().state;
|
||||||
final dictionaries = view.dictionaries;
|
final dictionaries = view.dictionaries;
|
||||||
final installed = dictionaries.installed;
|
if (dictionaries.installed == null &&
|
||||||
final size = view.offeredSize;
|
view.offeredSize == null &&
|
||||||
if (installed == null && size == null && dictionaries.progress == null) {
|
dictionaries.progress == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
final rows = [_LanguageRow(language: l10n.languageJapanese, view: view)];
|
||||||
return Padding(
|
return Padding(
|
||||||
// As Settings spaces its cards; none while it is hidden.
|
// As Settings spaces its cards; none while it is hidden.
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
|
@ -193,50 +198,81 @@ class const _SettingsCard() extends StatelessWidget {
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
for (final row in rows) ...[const SizedBox(height: 8), row],
|
||||||
if (dictionaries.progress case final progress?)
|
|
||||||
DeviceDictionaryProgressView(progress: progress)
|
|
||||||
else if (installed != null)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.settingsDeviceDictionaryHeld(
|
|
||||||
formatFileSize(l10n, installed.bytes),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: () =>
|
|
||||||
context.read<DeviceDictionaryCubit>().remove(),
|
|
||||||
child: Text(l10n.settingsDeviceDictionaryRemove),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
if (dictionaries.failed) ...[
|
|
||||||
Text(
|
|
||||||
l10n.settingsDeviceDictionaryFailed,
|
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
child: FilledButton.tonal(
|
|
||||||
onPressed: () =>
|
|
||||||
context.read<DeviceDictionaryCubit>().download(),
|
|
||||||
child: Text(
|
|
||||||
l10n.settingsDeviceDictionaryDownload(
|
|
||||||
formatFileSize(l10n, size!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A language's dictionaries on Settings' card: its name, with what the
|
||||||
|
/// device holds or that the download failed beneath, and at the end its
|
||||||
|
/// action: Download with its size, the download's progress, or Remove.
|
||||||
|
class const _LanguageRow({
|
||||||
|
required final String language,
|
||||||
|
required final DeviceDictionaryState view,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
/// How wide the download's progress is at the row's end.
|
||||||
|
static const _progressWidth = 180.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final dictionaries = view.dictionaries;
|
||||||
|
final installed = dictionaries.installed;
|
||||||
|
final progress = dictionaries.progress;
|
||||||
|
final note = switch ((progress, installed)) {
|
||||||
|
(null, final installed?) => (
|
||||||
|
l10n.settingsDeviceDictionaryHeld(
|
||||||
|
formatFileSize(l10n, installed.bytes),
|
||||||
|
),
|
||||||
|
theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
(null, null) when dictionaries.failed => (
|
||||||
|
l10n.settingsDeviceDictionaryFailed,
|
||||||
|
theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
final Widget action = switch ((progress, installed, view.offeredSize)) {
|
||||||
|
(final progress?, _, _) => SizedBox(
|
||||||
|
width: _progressWidth,
|
||||||
|
child: DeviceDictionaryProgressView(progress: progress),
|
||||||
|
),
|
||||||
|
(null, _?, _) => OutlinedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
unawaited(context.read<DeviceDictionaryCubit>().remove()),
|
||||||
|
child: Text(l10n.settingsDeviceDictionaryRemove),
|
||||||
|
),
|
||||||
|
(null, null, final size?) => FilledButton.tonal(
|
||||||
|
onPressed: () =>
|
||||||
|
unawaited(context.read<DeviceDictionaryCubit>().download()),
|
||||||
|
child: Text(
|
||||||
|
l10n.settingsDeviceDictionaryDownload(formatFileSize(l10n, size)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(null, null, null) => const SizedBox.shrink(),
|
||||||
|
};
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(language, style: theme.textTheme.bodyLarge),
|
||||||
|
if (note case (final text, final color))
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(color: color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
action,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ void main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Japanese on this device', () {
|
group('Dictionaries on this device', () {
|
||||||
/// Opens Settings, tall enough to show every card at once.
|
/// Opens Settings, tall enough to show every card at once.
|
||||||
Future<void> openSettings(
|
Future<void> openSettings(
|
||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
|
|
@ -133,7 +133,13 @@ void main() {
|
||||||
final gateway = FakeDeviceDictionaryGateway(offered: offeredJapanese());
|
final gateway = FakeDeviceDictionaryGateway(offered: offeredJapanese());
|
||||||
await openSettings(tester, gateway);
|
await openSettings(tester, gateway);
|
||||||
|
|
||||||
expect(find.text('Japanese on this device'), findsOneWidget);
|
expect(find.text('Dictionaries on this device'), findsOneWidget);
|
||||||
|
// One row a language, its name at the start and its action at the
|
||||||
|
// end, so the languages to come line up beneath (4 Nov 2026).
|
||||||
|
final language = tester.getRect(find.text('Japanese'));
|
||||||
|
final action = tester.getRect(find.text('Download (34.0 MB)'));
|
||||||
|
expect(language.right, lessThan(action.left));
|
||||||
|
expect(language.center.dy, closeTo(action.center.dy, 2));
|
||||||
gateway.fail = const SocketException('offline');
|
gateway.fail = const SocketException('offline');
|
||||||
await tester.tap(find.text('Download (34.0 MB)'));
|
await tester.tap(find.text('Download (34.0 MB)'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -156,6 +162,10 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('On this device: 150.0 MB'), findsOneWidget);
|
expect(find.text('On this device: 150.0 MB'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.text('Japanese')).right,
|
||||||
|
lessThan(tester.getRect(find.text('Remove')).left),
|
||||||
|
);
|
||||||
await tester.tap(find.text('Remove'));
|
await tester.tap(find.text('Remove'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|
@ -167,7 +177,7 @@ void main() {
|
||||||
'the device holds none', (tester) async {
|
'the device holds none', (tester) async {
|
||||||
await openSettings(tester, FakeDeviceDictionaryGateway());
|
await openSettings(tester, FakeDeviceDictionaryGateway());
|
||||||
|
|
||||||
expect(find.text('Japanese on this device'), findsNothing);
|
expect(find.text('Dictionaries on this device'), findsNothing);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue