feat(todo,meditate): what a setting does behind its question mark
The developer's review of 3 Nov 2026 (ADR 0033, D25): an explanation stands one tap away, behind halkyon_ui's HelpButton beside the setting's name, rather than always below it. Todo: resetting sub-tasks, the automatic reminder (its value stays below), counting a recurrence from completion. Meditate: keeping the screen on, the streak, the morning's dream prompt. What is the person's own (a value, a count) stays in view; an empty list's message and quick add's hint about highlighted words, shown only while there are some, stay too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
140a283d0e
commit
ef80453fa0
7 changed files with 102 additions and 16 deletions
|
|
@ -0,0 +1,19 @@
|
||||||
|
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,7 @@
|
||||||
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';
|
||||||
|
|
@ -67,8 +68,7 @@ class const SittingSettingsCard({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.sunMedium),
|
secondary: const Icon(LucideIcons.sunMedium),
|
||||||
title: Text(l10n.keepScreenOnTitle),
|
title: TitleWithHelp(l10n.keepScreenOnTitle, l10n.keepScreenOnHint),
|
||||||
subtitle: Text(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,8 +125,7 @@ class const PracticeSettingsCard({super.key}) extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.flame),
|
secondary: const Icon(LucideIcons.flame),
|
||||||
title: Text(l10n.showStreakTitle),
|
title: TitleWithHelp(l10n.showStreakTitle, l10n.showStreakHint),
|
||||||
subtitle: Text(l10n.showStreakHint),
|
|
||||||
value: settings.showStreak,
|
value: settings.showStreak,
|
||||||
onChanged: (on) => _write(context, settings.copyWith(showStreak: on)),
|
onChanged: (on) => _write(context, settings.copyWith(showStreak: on)),
|
||||||
),
|
),
|
||||||
|
|
@ -149,8 +148,7 @@ class const JournalSettingsCard({super.key}) extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.sunrise),
|
secondary: const Icon(LucideIcons.sunrise),
|
||||||
title: Text(l10n.dreamPromptTitle),
|
title: TitleWithHelp(l10n.dreamPromptTitle, l10n.dreamPromptHint),
|
||||||
subtitle: Text(l10n.dreamPromptHint),
|
|
||||||
value: settings.dreamPrompt,
|
value: settings.dreamPrompt,
|
||||||
onChanged: (on) =>
|
onChanged: (on) =>
|
||||||
_write(context, settings.copyWith(dreamPrompt: on)),
|
_write(context, settings.copyWith(dreamPrompt: on)),
|
||||||
|
|
|
||||||
|
|
@ -660,6 +660,23 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Settings of sitting (ADR 0038, D11)', () {
|
group('Settings of sitting (ADR 0038, D11)', () {
|
||||||
|
testWidgets('what a setting does stays behind its question mark until '
|
||||||
|
"asked (the developer's review, 3 Nov 2026)", (tester) async {
|
||||||
|
await start(tester, usedBefore: true);
|
||||||
|
await openSettings(tester);
|
||||||
|
expect(find.text('While a sitting runs'), findsNothing);
|
||||||
|
|
||||||
|
final help = find.descendant(
|
||||||
|
of: find.widgetWithText(SwitchListTile, 'Keep the screen on'),
|
||||||
|
matching: find.byType(HelpButton),
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(help);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(help);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('While a sitting runs'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('the bell picked, the bells between and the screen are kept '
|
testWidgets('the bell picked, the bells between and the screen are kept '
|
||||||
"in the person's sealed settings", (tester) async {
|
"in the person's sealed settings", (tester) async {
|
||||||
await start(tester, usedBefore: true);
|
await start(tester, usedBefore: true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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,5 +1,6 @@
|
||||||
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';
|
||||||
|
|
@ -352,8 +353,10 @@ class _RepeatEditorState() extends State<_RepeatEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(l10n.repeatCountFromCompletion),
|
title: TitleWithHelp(
|
||||||
subtitle: Text(l10n.repeatCountFromCompletionHint),
|
l10n.repeatCountFromCompletion,
|
||||||
|
l10n.repeatCountFromCompletionHint,
|
||||||
|
),
|
||||||
value: _fromCompletion,
|
value: _fromCompletion,
|
||||||
onChanged: (on) => setState(() => _fromCompletion = on),
|
onChanged: (on) => setState(() => _fromCompletion = on),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ 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';
|
||||||
|
|
@ -62,8 +63,10 @@ class const TaskSettingsCard({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.listRestart),
|
secondary: const Icon(LucideIcons.listRestart),
|
||||||
title: Text(l10n.resetSubTasksTitle),
|
title: TitleWithHelp(
|
||||||
subtitle: Text(l10n.resetSubTasksHint),
|
l10n.resetSubTasksTitle,
|
||||||
|
l10n.resetSubTasksHint,
|
||||||
|
),
|
||||||
value: tasks.resetSubTasks,
|
value: tasks.resetSubTasks,
|
||||||
onChanged: (on) => _write(
|
onChanged: (on) => _write(
|
||||||
context,
|
context,
|
||||||
|
|
@ -94,12 +97,11 @@ class const ReminderSettingsCard({super.key}) extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(LucideIcons.alarmClockPlus),
|
leading: const Icon(LucideIcons.alarmClockPlus),
|
||||||
title: Text(l10n.automaticReminderTitle),
|
title: TitleWithHelp(
|
||||||
subtitle: Text(
|
l10n.automaticReminderTitle,
|
||||||
'${automatic(reminders.automatic)}\n'
|
l10n.automaticReminderHint,
|
||||||
'${l10n.automaticReminderHint}',
|
|
||||||
),
|
),
|
||||||
isThreeLine: true,
|
subtitle: Text(automatic(reminders.automatic)),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final picked = await _pick<int?>(
|
final picked = await _pick<int?>(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -2712,6 +2712,28 @@ void main() {
|
||||||
() => SettingsRepository(settingsRows, keys).read(),
|
() => SettingsRepository(settingsRows, keys).read(),
|
||||||
))!;
|
))!;
|
||||||
|
|
||||||
|
testWidgets('what a setting does stays behind its question mark until '
|
||||||
|
"asked (the developer's review, 3 Nov 2026)", (tester) async {
|
||||||
|
await start(tester, usedBefore: true);
|
||||||
|
await openSettings(tester);
|
||||||
|
const hint = 'Open them again when a recurring task is done';
|
||||||
|
expect(find.text(hint), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.textContaining('unless it has reminders of its own'),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
|
||||||
|
final help = find.descendant(
|
||||||
|
of: find.widgetWithText(SwitchListTile, 'Reset sub-tasks'),
|
||||||
|
matching: find.byType(HelpButton),
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(help);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(help);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(hint), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Tasks and Reminders come before the account; a week '
|
testWidgets('Tasks and Reminders come before the account; a week '
|
||||||
"started on Sunday starts Upcoming's strip on it, kept for every "
|
"started on Sunday starts Upcoming's strip on it, kept for every "
|
||||||
'device', (tester) async {
|
'device', (tester) async {
|
||||||
|
|
@ -2750,7 +2772,13 @@ void main() {
|
||||||
'reminders are snoozed for as long as set', (tester) async {
|
'reminders are snoozed for as long as set', (tester) async {
|
||||||
await start(tester, usedBefore: true);
|
await start(tester, usedBefore: true);
|
||||||
await openSettings(tester);
|
await openSettings(tester);
|
||||||
expect(find.textContaining(RegExp(r'^None\n')), findsOneWidget);
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.widgetWithText(ListTile, 'Automatic reminder'),
|
||||||
|
matching: find.text('None'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
await choose(tester, 'Automatic reminder', '30 minutes before');
|
await choose(tester, 'Automatic reminder', '30 minutes before');
|
||||||
await choose(tester, 'When snoozed', '10 minutes');
|
await choose(tester, 'When snoozed', '10 minutes');
|
||||||
final settings = await stored(tester);
|
final settings = await stored(tester);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue