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:
parent
81adeb91cc
commit
9dcb0dbdc3
10 changed files with 26 additions and 81 deletions
|
|
@ -1,19 +0,0 @@
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
|
||||||
import 'package:material_ui/material_ui.dart';
|
|
||||||
|
|
||||||
/// A setting's [title] with the question mark that explains it ([help],
|
|
||||||
/// halkyon_ui's [HelpButton]) beside it, rather than the explanation always
|
|
||||||
/// below (the developer's review, 3 Nov 2026).
|
|
||||||
class const TitleWithHelp(
|
|
||||||
final String title,
|
|
||||||
final String help, {
|
|
||||||
super.key,
|
|
||||||
}) extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Row(
|
|
||||||
children: [
|
|
||||||
Flexible(child: Text(title)),
|
|
||||||
HelpButton(help, title: title),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/core/privacy/meditate_keys.dart';
|
import 'package:halkyon_meditate_flutter/src/core/privacy/meditate_keys.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/core/widget/title_with_help.dart';
|
|
||||||
import 'package:halkyon_meditate_flutter/src/feature/practice/widget/practice_texts.dart';
|
import 'package:halkyon_meditate_flutter/src/feature/practice/widget/practice_texts.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/feature/settings/model/meditate_settings.dart';
|
import 'package:halkyon_meditate_flutter/src/feature/settings/model/meditate_settings.dart';
|
||||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/bloc/practice_cubit.dart';
|
import 'package:halkyon_meditate_flutter/src/feature/sitting/bloc/practice_cubit.dart';
|
||||||
|
|
@ -68,7 +67,10 @@ class const SittingSettingsCard({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.sunMedium),
|
secondary: const Icon(LucideIcons.sunMedium),
|
||||||
title: TitleWithHelp(l10n.keepScreenOnTitle, l10n.keepScreenOnHint),
|
title: ExplainedTitle(
|
||||||
|
l10n.keepScreenOnTitle,
|
||||||
|
help: l10n.keepScreenOnHint,
|
||||||
|
),
|
||||||
value: settings.keepScreenOn,
|
value: settings.keepScreenOn,
|
||||||
onChanged: (on) =>
|
onChanged: (on) =>
|
||||||
_write(context, settings.copyWith(keepScreenOn: on)),
|
_write(context, settings.copyWith(keepScreenOn: on)),
|
||||||
|
|
@ -125,7 +127,10 @@ class const PracticeSettingsCard({super.key}) extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.flame),
|
secondary: const Icon(LucideIcons.flame),
|
||||||
title: TitleWithHelp(l10n.showStreakTitle, l10n.showStreakHint),
|
title: ExplainedTitle(
|
||||||
|
l10n.showStreakTitle,
|
||||||
|
help: l10n.showStreakHint,
|
||||||
|
),
|
||||||
value: settings.showStreak,
|
value: settings.showStreak,
|
||||||
onChanged: (on) => _write(context, settings.copyWith(showStreak: on)),
|
onChanged: (on) => _write(context, settings.copyWith(showStreak: on)),
|
||||||
),
|
),
|
||||||
|
|
@ -148,7 +153,10 @@ class const JournalSettingsCard({super.key}) extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.sunrise),
|
secondary: const Icon(LucideIcons.sunrise),
|
||||||
title: TitleWithHelp(l10n.dreamPromptTitle, l10n.dreamPromptHint),
|
title: ExplainedTitle(
|
||||||
|
l10n.dreamPromptTitle,
|
||||||
|
help: l10n.dreamPromptHint,
|
||||||
|
),
|
||||||
value: settings.dreamPrompt,
|
value: settings.dreamPrompt,
|
||||||
onChanged: (on) =>
|
onChanged: (on) =>
|
||||||
_write(context, settings.copyWith(dreamPrompt: on)),
|
_write(context, settings.copyWith(dreamPrompt: on)),
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
|
||||||
import 'package:material_ui/material_ui.dart';
|
|
||||||
|
|
||||||
/// A setting's [title] with the question mark that explains it ([help],
|
|
||||||
/// halkyon_ui's [HelpButton]) beside it, rather than the explanation always
|
|
||||||
/// below (the developer's review, 3 Nov 2026).
|
|
||||||
class const TitleWithHelp(
|
|
||||||
final String title,
|
|
||||||
final String help, {
|
|
||||||
super.key,
|
|
||||||
}) extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Row(
|
|
||||||
children: [
|
|
||||||
Flexible(child: Text(title)),
|
|
||||||
HelpButton(help, title: title),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/core/widget/title_with_help.dart';
|
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
||||||
|
|
@ -353,9 +352,9 @@ class _RepeatEditorState() extends State<_RepeatEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: TitleWithHelp(
|
title: ExplainedTitle(
|
||||||
l10n.repeatCountFromCompletion,
|
l10n.repeatCountFromCompletion,
|
||||||
l10n.repeatCountFromCompletionHint,
|
help: l10n.repeatCountFromCompletionHint,
|
||||||
),
|
),
|
||||||
value: _fromCompletion,
|
value: _fromCompletion,
|
||||||
onChanged: (on) => setState(() => _fromCompletion = on),
|
onChanged: (on) => setState(() => _fromCompletion = on),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/core/widget/title_with_help.dart';
|
|
||||||
import 'package:halkyon_todo_flutter/src/feature/reminder/model/snooze.dart';
|
import 'package:halkyon_todo_flutter/src/feature/reminder/model/snooze.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/reminder/widget/reminder_picker.dart';
|
import 'package:halkyon_todo_flutter/src/feature/reminder/widget/reminder_picker.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
|
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
|
||||||
|
|
@ -63,9 +62,9 @@ class const TaskSettingsCard({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.listRestart),
|
secondary: const Icon(LucideIcons.listRestart),
|
||||||
title: TitleWithHelp(
|
title: ExplainedTitle(
|
||||||
l10n.resetSubTasksTitle,
|
l10n.resetSubTasksTitle,
|
||||||
l10n.resetSubTasksHint,
|
help: l10n.resetSubTasksHint,
|
||||||
),
|
),
|
||||||
value: tasks.resetSubTasks,
|
value: tasks.resetSubTasks,
|
||||||
onChanged: (on) => _write(
|
onChanged: (on) => _write(
|
||||||
|
|
@ -97,9 +96,9 @@ class const ReminderSettingsCard({super.key}) extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(LucideIcons.alarmClockPlus),
|
leading: const Icon(LucideIcons.alarmClockPlus),
|
||||||
title: TitleWithHelp(
|
title: ExplainedTitle(
|
||||||
l10n.automaticReminderTitle,
|
l10n.automaticReminderTitle,
|
||||||
l10n.automaticReminderHint,
|
help: l10n.automaticReminderHint,
|
||||||
),
|
),
|
||||||
subtitle: Text(automatic(reminders.automatic)),
|
subtitle: Text(automatic(reminders.automatic)),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_train_flutter/l10n/app_localizations.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/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_cubit.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/loop/bloc/loop_setup_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';
|
import 'package:halkyon_train_flutter/src/feature/loop/data/loop_platform.dart';
|
||||||
|
|
@ -208,7 +207,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
|
||||||
return [
|
return [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.activity),
|
secondary: const Icon(LucideIcons.activity),
|
||||||
title: HelpTitle(l10n.loopDetection, help: l10n.loopDetectionHint),
|
title: ExplainedTitle(l10n.loopDetection, help: l10n.loopDetectionHint),
|
||||||
value: on,
|
value: on,
|
||||||
onChanged: usable
|
onChanged: usable
|
||||||
? (on) => unawaited(
|
? (on) => unawaited(
|
||||||
|
|
@ -303,7 +302,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(LucideIcons.search),
|
leading: const Icon(LucideIcons.search),
|
||||||
title: HelpTitle(
|
title: ExplainedTitle(
|
||||||
l10n.loopFirmwareCheck,
|
l10n.loopFirmwareCheck,
|
||||||
help: through
|
help: through
|
||||||
? l10n.loopFirmwareViaInstance
|
? l10n.loopFirmwareViaInstance
|
||||||
|
|
@ -316,7 +315,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
|
||||||
if (state.status.relayOffered || through)
|
if (state.status.relayOffered || through)
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.server),
|
secondary: const Icon(LucideIcons.server),
|
||||||
title: HelpTitle(
|
title: ExplainedTitle(
|
||||||
l10n.loopFirmwareThroughInstance,
|
l10n.loopFirmwareThroughInstance,
|
||||||
help: l10n.loopFirmwareThroughInstanceHint,
|
help: l10n.loopFirmwareThroughInstanceHint,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_train_flutter/l10n/app_localizations.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/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/bloc/movement_cubit.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/movement/model/movement_settings.dart';
|
import 'package:halkyon_train_flutter/src/feature/movement/model/movement_settings.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
|
@ -99,7 +98,7 @@ class const MovementScreen({super.key}) extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: HelpTitle(
|
title: ExplainedTitle(
|
||||||
l10n.movementRemind,
|
l10n.movementRemind,
|
||||||
help: l10n.movementRemindHint,
|
help: l10n.movementRemindHint,
|
||||||
),
|
),
|
||||||
|
|
@ -197,7 +196,7 @@ class const MovementScreen({super.key}) extends StatelessWidget {
|
||||||
SectionCard(
|
SectionCard(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
title: HelpTitle(
|
title: ExplainedTitle(
|
||||||
l10n.movementSuggest,
|
l10n.movementSuggest,
|
||||||
help: l10n.movementSuggestHint,
|
help: l10n.movementSuggestHint,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_train_flutter/l10n/app_localizations.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/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/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/jellyfin_client.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_downloads.dart';
|
import 'package:halkyon_train_flutter/src/feature/music/data/music_downloads.dart';
|
||||||
|
|
@ -265,7 +264,7 @@ class const _Playlists({
|
||||||
ListTile(
|
ListTile(
|
||||||
title: DefaultTextStyle.merge(
|
title: DefaultTextStyle.merge(
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
child: HelpTitle(
|
child: ExplainedTitle(
|
||||||
l10n.musicPlaylists,
|
l10n.musicPlaylists,
|
||||||
help: l10n.musicPlaylistsHint,
|
help: l10n.musicPlaylistsHint,
|
||||||
),
|
),
|
||||||
|
|
@ -290,7 +289,7 @@ class const _Playlists({
|
||||||
_Kept(kept: kept),
|
_Kept(kept: kept),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.fileAudio),
|
secondary: const Icon(LucideIcons.fileAudio),
|
||||||
title: HelpTitle(l10n.musicSmaller, help: l10n.musicSmallerHint),
|
title: ExplainedTitle(l10n.musicSmaller, help: l10n.musicSmallerHint),
|
||||||
value: settings.smaller,
|
value: settings.smaller,
|
||||||
onChanged: (on) => cubit().keepSmaller(on: on),
|
onChanged: (on) => cubit().keepSmaller(on: on),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue