fix(jellyfin): Add and Jisho on the word's own line, entries unfold after 0.6 s

The Watch screen's entries carry Jisho's link and Add at the end of the headword's line, aligned right, rather than beside the senses; and an entry unfolds over the list once the pointer has rested on it for 0.6 s, as 0.3 s opened entries the pointer only passed over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-13 19:23:17 +02:00
commit 747d73b867
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 61 additions and 48 deletions

View file

@ -11,13 +11,14 @@ import 'package:url_launcher/url_launcher.dart';
/// A word as a dictionary lists it: its headword and reading, [progress]
/// beside them if given, its senses one to a line with their grammar notes
/// small, grey and slanted, cut short at two lines unless [whole], its
/// level and whether it is common; [trailing] at its end, and [onTap] for
/// the whole row.
/// level and whether it is common; [trailing] at its end, [actions] at the
/// end of the headword's own line, and [onTap] for the whole row.
class const DictionaryWordTile({
required final DictionaryWord word,
final Widget? trailing,
final VoidCallback? onTap,
final Widget? progress,
final List<Widget> actions = const [],
final bool whole = false,
super.key,
}) extends StatelessWidget {
@ -31,10 +32,16 @@ class const DictionaryWordTile({
];
return ListTile(
onTap: onTap,
title: Text.rich(
title: Row(
children: [
Expanded(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: word.headword, style: theme.textTheme.titleMedium),
TextSpan(
text: word.headword,
style: theme.textTheme.titleMedium,
),
if (word.reading case final reading?)
TextSpan(
text: ' $reading',
@ -53,6 +60,10 @@ class const DictionaryWordTile({
],
),
),
),
...actions,
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View file

@ -607,10 +607,15 @@ class const _ChosenWord({required final Dictionary dictionary})
return const SizedBox();
}
final translation = state.translationOf(selected.line);
Widget? trailing(DictionaryWord entry, String form) =>
state.known.contains(entry.ref)
? null
: FilledButton.tonal(
// On the headword's line, at its end (decided 2026-10-13): Jisho, and
// Add for a word the studyset lacks.
List<Widget> actions(DictionaryWord entry, String form) => [
_JishoLink(word: entry),
if (!state.known.contains(entry.ref))
FilledButton.tonal(
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
await cubit().add(entry, form: form);
@ -619,27 +624,23 @@ class const _ChosenWord({required final Dictionary dictionary})
);
},
child: Text(l10n.watchAdd),
);
Widget beside(DictionaryWord word) => Row(
mainAxisSize: MainAxisSize.min,
children: [
if (state.known.contains(word.ref))
_ProgressChip(schedule: state.progress[word.ref]),
_JishoLink(word: word),
],
);
),
];
Widget? beside(DictionaryWord word) => state.known.contains(word.ref)
? _ProgressChip(schedule: state.progress[word.ref])
: null;
Widget row(DictionaryWord entry, String form) => _Unfolding(
word: entry,
folded: DictionaryWordTile(
word: entry,
progress: beside(entry),
trailing: trailing(entry, form),
actions: actions(entry, form),
),
whole: DictionaryWordTile(
word: entry,
whole: true,
progress: beside(entry),
trailing: trailing(entry, form),
actions: actions(entry, form),
),
);
return ListView(
@ -703,8 +704,9 @@ class const _Unfolding({
required final Widget folded,
required final Widget whole,
}) extends StatefulWidget {
/// How long the pointer rests on an entry before it unfolds.
static const rest = Duration(milliseconds: 300);
/// How long the pointer rests on an entry before it unfolds (decided
/// 2026-10-13: 0.3 s opened entries the pointer only passed over).
static const rest = Duration(milliseconds: 600);
@override
State<_Unfolding> createState() => _UnfoldingState();
@ -769,7 +771,7 @@ class _UnfoldingState() extends State<_Unfolding> {
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, box) {
// The senses' room: the row less its padding and what is beside it.
if (!_folds(context, box.maxWidth - 140)) {
if (!_folds(context, box.maxWidth - 40)) {
return widget.folded;
}
final lit = _VideoColour.litOf(context);

View file

@ -534,7 +534,7 @@ void main() {
await mouse.moveTo(
tester.getCenter(find.textContaining('sense 1 of a long').first),
);
await tester.pump(const Duration(milliseconds: 400));
await tester.pump(const Duration(milliseconds: 700));
await tester.pumpAndSettle();
expect(find.byType(VocabularySensesView), findsOneWidget);
expect(find.text('station, as in sense 6 of a long entry'), findsOneWidget);