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:
Tizian 「ティツィアーン」 2026-10-12 22:41:09 +02:00
commit 44cd1327d2
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
9 changed files with 127 additions and 21 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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:

View file

@ -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';

View file

@ -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';

View file

@ -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),
],
),
);

View file

@ -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),
],
),
),

View file

@ -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,
),
),
),
],
),
),
],
),
);
},
),
);
}
}

View file

@ -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();