refactor(todo,meditate,train): a title with its question mark is halkyon_ui's ExplainedTitle

Three forks of the 3 Nov review each wrote the same widget (Todo's and
Meditate's TitleWithHelp, Train's HelpTitle); halkyon_ui's ExplainedTitle,
from Learn's, takes their place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 23:18:11 +01:00
commit 9dcb0dbdc3
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 26 additions and 81 deletions

View file

@ -1,19 +0,0 @@
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// A tile's [title] with its explanation, [help], behind halkyon_ui's
/// [HelpButton] beside it rather than under it (ADR 0030, D64): what
/// explains is one tap away, what is the person's own stays in sight.
class const HelpTitle(
final String title, {
required final String help,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) => Row(
children: [
Flexible(child: Text(title)),
HelpButton(help, title: title),
],
);
}

View file

@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/core/widget/help_title.dart';
import 'package:halkyon_train_flutter/src/feature/loop/bloc/loop_cubit.dart';
import 'package:halkyon_train_flutter/src/feature/loop/bloc/loop_setup_cubit.dart';
import 'package:halkyon_train_flutter/src/feature/loop/data/loop_platform.dart';
@ -208,7 +207,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
return [
SwitchListTile(
secondary: const Icon(LucideIcons.activity),
title: HelpTitle(l10n.loopDetection, help: l10n.loopDetectionHint),
title: ExplainedTitle(l10n.loopDetection, help: l10n.loopDetectionHint),
value: on,
onChanged: usable
? (on) => unawaited(
@ -303,7 +302,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
),
ListTile(
leading: const Icon(LucideIcons.search),
title: HelpTitle(
title: ExplainedTitle(
l10n.loopFirmwareCheck,
help: through
? l10n.loopFirmwareViaInstance
@ -316,7 +315,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
if (state.status.relayOffered || through)
SwitchListTile(
secondary: const Icon(LucideIcons.server),
title: HelpTitle(
title: ExplainedTitle(
l10n.loopFirmwareThroughInstance,
help: l10n.loopFirmwareThroughInstanceHint,
),

View file

@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/core/widget/help_title.dart';
import 'package:halkyon_train_flutter/src/feature/movement/bloc/movement_cubit.dart';
import 'package:halkyon_train_flutter/src/feature/movement/model/movement_settings.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
@ -99,7 +98,7 @@ class const MovementScreen({super.key}) extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SwitchListTile(
title: HelpTitle(
title: ExplainedTitle(
l10n.movementRemind,
help: l10n.movementRemindHint,
),
@ -197,7 +196,7 @@ class const MovementScreen({super.key}) extends StatelessWidget {
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SwitchListTile(
title: HelpTitle(
title: ExplainedTitle(
l10n.movementSuggest,
help: l10n.movementSuggestHint,
),

View file

@ -2,7 +2,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/core/widget/help_title.dart';
import 'package:halkyon_train_flutter/src/feature/music/bloc/music_cubit.dart';
import 'package:halkyon_train_flutter/src/feature/music/data/jellyfin_client.dart';
import 'package:halkyon_train_flutter/src/feature/music/data/music_downloads.dart';
@ -265,7 +264,7 @@ class const _Playlists({
ListTile(
title: DefaultTextStyle.merge(
style: Theme.of(context).textTheme.titleSmall,
child: HelpTitle(
child: ExplainedTitle(
l10n.musicPlaylists,
help: l10n.musicPlaylistsHint,
),
@ -290,7 +289,7 @@ class const _Playlists({
_Kept(kept: kept),
SwitchListTile(
secondary: const Icon(LucideIcons.fileAudio),
title: HelpTitle(l10n.musicSmaller, help: l10n.musicSmallerHint),
title: ExplainedTitle(l10n.musicSmaller, help: l10n.musicSmallerHint),
value: settings.smaller,
onChanged: (on) => cubit().keepSmaller(on: on),
),

View file

@ -0,0 +1,44 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets("a setting's title keeps its explanation behind the question "
'mark, and a tap on the title still reaches the tile', (tester) async {
var on = false;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: HalkyonUiLocalizations.localizationsDelegates,
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) => SwitchListTile(
title: const ExplainedTitle(
'Smaller copies',
help: 'Lossless files kept as Opus.',
),
value: on,
onChanged: (value) => setState(() => on = value),
),
),
),
),
);
expect(find.text('Lossless files kept as Opus.'), findsNothing);
await tester.tap(find.text('Smaller copies'));
await tester.pump();
expect(on, isTrue);
await tester.tap(find.byType(HelpButton));
await tester.pumpAndSettle();
expect(find.text('Lossless files kept as Opus.'), findsOneWidget);
expect(
find.descendant(
of: find.byType(AlertDialog),
matching: find.text('Smaller copies'),
),
findsOneWidget,
);
expect(on, isTrue, reason: 'the question mark leaves the switch alone');
});
}