fix(dictionary): its sources in a short line, the full credits a tap away
The foot of the dictionary named each source with its makers and
licence, three lines of small print. It now names the sources alone in
a line ("Words from JMdict, JLPT vocabulary lists, Tatoeba"); tapping
it or its ⓘ shows who made each, under which licence, with its link,
which is still where the entries show, as the licences ask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ec48eaa6cf
commit
44cd1327d2
9 changed files with 127 additions and 21 deletions
|
|
@ -640,6 +640,7 @@
|
|||
"dictionaryConfirmMessage": "Jedes wird eine Karteikarte in diesem Lernset.",
|
||||
"dictionaryConfirmAdd": "Hinzufügen",
|
||||
"dictionaryCredits": "Wörter aus {sources}.",
|
||||
"dictionarySourcesTitle": "Quellen",
|
||||
"dictionaryLookUp": "Im Wörterbuch nachschlagen",
|
||||
"dictionaryCharacterSearchHint": "Ein Kanji oder Kana, eine Lesung in Kana oder Romaji, oder eine Bedeutung",
|
||||
"dictionaryTabWords": "Wörter",
|
||||
|
|
|
|||
|
|
@ -731,6 +731,7 @@
|
|||
"dictionaryConfirmMessage": "Each becomes a flashcard in this studyset.",
|
||||
"dictionaryConfirmAdd": "Add",
|
||||
"dictionaryCredits": "Words from {sources}.",
|
||||
"dictionarySourcesTitle": "Sources",
|
||||
"@dictionaryCredits": {"placeholders": {"sources": {"type": "String"}}},
|
||||
"dictionaryLookUp": "Look up in the dictionary",
|
||||
"dictionaryCharacterSearchHint": "A kanji or kana, a reading in kana or romaji, or a meaning",
|
||||
|
|
|
|||
|
|
@ -3826,6 +3826,12 @@ abstract class AppLocalizations {
|
|||
/// **'Words from {sources}.'**
|
||||
String dictionaryCredits(String sources);
|
||||
|
||||
/// No description provided for @dictionarySourcesTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sources'**
|
||||
String get dictionarySourcesTitle;
|
||||
|
||||
/// No description provided for @dictionaryLookUp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -2364,6 +2364,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return 'Wörter aus $sources.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dictionarySourcesTitle => 'Quellen';
|
||||
|
||||
@override
|
||||
String get dictionaryLookUp => 'Im Wörterbuch nachschlagen';
|
||||
|
||||
|
|
|
|||
|
|
@ -2346,6 +2346,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return 'Words from $sources.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dictionarySourcesTitle => 'Sources';
|
||||
|
||||
@override
|
||||
String get dictionaryLookUp => 'Look up in the dictionary';
|
||||
|
||||
|
|
|
|||
|
|
@ -160,13 +160,11 @@ class _LookUpState() extends State<_LookUp> {
|
|||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
dictionaryCreditLine(l10n, widget.dictionary),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
DictionarySources(
|
||||
dictionary: widget.dictionary,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -262,7 +262,6 @@ class const _Page({
|
|||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: readableContentMaxWidth),
|
||||
|
|
@ -271,15 +270,7 @@ class const _Page({
|
|||
children: [
|
||||
...children,
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||
child: Text(
|
||||
dictionaryCreditLine(AppLocalizations.of(context), dictionary),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
DictionarySources(dictionary: dictionary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/vocabulary_senses.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// A word as a dictionary lists it: its headword and reading, its senses
|
||||
/// with their grammar notes small, grey and slanted, cut short at two
|
||||
|
|
@ -176,14 +181,104 @@ Widget dictionaryItemTile(
|
|||
),
|
||||
};
|
||||
|
||||
/// The credits of [dictionary] in a line, as its licence asks to name them.
|
||||
/// The sources of [dictionary] in a line, by their names alone.
|
||||
String dictionaryCreditLine(AppLocalizations l10n, Dictionary dictionary) {
|
||||
final sources = [
|
||||
for (final credit in dictionary.credits)
|
||||
'${credit.name} (${credit.by}, ${credit.license})',
|
||||
].join('; ');
|
||||
final sources = {
|
||||
for (final credit in dictionary.credits) credit.name,
|
||||
}.join(', ');
|
||||
return switch (dictionary.kind) {
|
||||
DictionaryKind.words => l10n.dictionaryCredits(sources),
|
||||
DictionaryKind.characters => l10n.dictionaryCharacterCredits(sources),
|
||||
};
|
||||
}
|
||||
|
||||
/// The credits of [dictionary] where its entries show, as their licences
|
||||
/// ask: its sources by name in a short line, and a tap on it (or its ⓘ)
|
||||
/// shows who made each, under which licence, and where it is found.
|
||||
class const DictionarySources({
|
||||
required final Dictionary dictionary,
|
||||
final EdgeInsetsGeometry padding = const EdgeInsets.fromLTRB(16, 4, 4, 4),
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final muted = theme.colorScheme.onSurfaceVariant;
|
||||
return InkWell(
|
||||
onTap: () => _showCredits(context),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
dictionaryCreditLine(l10n, dictionary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: muted),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.dictionarySourcesTitle,
|
||||
icon: Icon(LucideIcons.info, size: 18, color: muted),
|
||||
onPressed: () => _showCredits(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCredits(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
unawaited(
|
||||
showAdaptiveSheet<void>(
|
||||
context,
|
||||
builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return SheetScaffold(
|
||||
title: l10n.dictionarySourcesTitle,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final credit in dictionary.credits)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(credit.name, style: theme.textTheme.titleSmall),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${credit.by} · ${credit.license}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (Uri.tryParse(credit.url) case final url?
|
||||
when url.isScheme('https') || url.isScheme('http'))
|
||||
InkWell(
|
||||
onTap: () => launchUrl(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Text(
|
||||
credit.url,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,15 @@ void main() {
|
|||
await show(tester);
|
||||
await addMaterial(tester, 'From the dictionary');
|
||||
|
||||
// Its sources by name in a line; who made them, under which licence,
|
||||
// a tap away.
|
||||
expect(find.textContaining('Words from JMdict'), findsOneWidget);
|
||||
expect(find.textContaining('EDRDG'), findsNothing);
|
||||
await tester.tap(find.byTooltip('Sources'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('EDRDG'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'たべ');
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
Loading…
Reference in a new issue