feat(train): the settings' explanations behind a question mark (ADR 0030, D63)
What a switch or a section explains, rather than what is the person's own, is one tap away: `HelpTitle`, a tile's title with halkyon_ui's `HelpButton` beside it, in place of the explaining subtitles of Moving more's reminders and suggestions, the music's playlists and smaller copies, and the Loop's detection, firmware check and firmware through the instance. Health Connect's short hints stay (they name what each switch does, and its "taken back" state takes their place), as do its rationale, the Loop's warnings before an update or a reset and the settings' one-line hints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b975659690
commit
b7db078c8e
7 changed files with 123 additions and 19 deletions
|
|
@ -0,0 +1,19 @@
|
||||||
|
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, D63): 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,6 +4,7 @@ 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';
|
||||||
|
|
@ -207,8 +208,7 @@ class const LoopScreen({super.key}) extends StatelessWidget {
|
||||||
return [
|
return [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.activity),
|
secondary: const Icon(LucideIcons.activity),
|
||||||
title: Text(l10n.loopDetection),
|
title: HelpTitle(l10n.loopDetection, help: l10n.loopDetectionHint),
|
||||||
subtitle: Text(l10n.loopDetectionHint),
|
|
||||||
value: on,
|
value: on,
|
||||||
onChanged: usable
|
onChanged: usable
|
||||||
? (on) => unawaited(
|
? (on) => unawaited(
|
||||||
|
|
@ -303,9 +303,11 @@ class const LoopScreen({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(LucideIcons.search),
|
leading: const Icon(LucideIcons.search),
|
||||||
title: Text(l10n.loopFirmwareCheck),
|
title: HelpTitle(
|
||||||
subtitle: Text(
|
l10n.loopFirmwareCheck,
|
||||||
through ? l10n.loopFirmwareViaInstance : l10n.loopFirmwareViaPolar,
|
help: through
|
||||||
|
? l10n.loopFirmwareViaInstance
|
||||||
|
: l10n.loopFirmwareViaPolar,
|
||||||
),
|
),
|
||||||
onTap: state.busy || stage != null || !state.status.connected
|
onTap: state.busy || stage != null || !state.status.connected
|
||||||
? null
|
? null
|
||||||
|
|
@ -314,8 +316,10 @@ 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: Text(l10n.loopFirmwareThroughInstance),
|
title: HelpTitle(
|
||||||
subtitle: Text(l10n.loopFirmwareThroughInstanceHint),
|
l10n.loopFirmwareThroughInstance,
|
||||||
|
help: l10n.loopFirmwareThroughInstanceHint,
|
||||||
|
),
|
||||||
value: through,
|
value: through,
|
||||||
onChanged: stage != null
|
onChanged: stage != null
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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';
|
||||||
|
|
@ -98,8 +99,10 @@ class const MovementScreen({super.key}) extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(l10n.movementRemind),
|
title: HelpTitle(
|
||||||
subtitle: Text(l10n.movementRemindHint),
|
l10n.movementRemind,
|
||||||
|
help: l10n.movementRemindHint,
|
||||||
|
),
|
||||||
value: settings.remind,
|
value: settings.remind,
|
||||||
onChanged: usable
|
onChanged: usable
|
||||||
? (on) => change(settings.copyWith(remind: on))
|
? (on) => change(settings.copyWith(remind: on))
|
||||||
|
|
@ -194,8 +197,10 @@ 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: Text(l10n.movementSuggest),
|
title: HelpTitle(
|
||||||
subtitle: Text(l10n.movementSuggestHint),
|
l10n.movementSuggest,
|
||||||
|
help: l10n.movementSuggestHint,
|
||||||
|
),
|
||||||
value: settings.suggest,
|
value: settings.suggest,
|
||||||
onChanged: usable
|
onChanged: usable
|
||||||
? (on) => change(settings.copyWith(suggest: on))
|
? (on) => change(settings.copyWith(suggest: on))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ 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';
|
||||||
|
|
@ -262,11 +263,13 @@ class const _Playlists({
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: DefaultTextStyle.merge(
|
||||||
l10n.musicPlaylists,
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
child: HelpTitle(
|
||||||
|
l10n.musicPlaylists,
|
||||||
|
help: l10n.musicPlaylistsHint,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(l10n.musicPlaylistsHint),
|
|
||||||
),
|
),
|
||||||
if (failure != null)
|
if (failure != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
@ -287,8 +290,7 @@ class const _Playlists({
|
||||||
_Kept(kept: kept),
|
_Kept(kept: kept),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(LucideIcons.fileAudio),
|
secondary: const Icon(LucideIcons.fileAudio),
|
||||||
title: Text(l10n.musicSmaller),
|
title: HelpTitle(l10n.musicSmaller, help: l10n.musicSmallerHint),
|
||||||
subtitle: Text(l10n.musicSmallerHint),
|
|
||||||
value: settings.smaller,
|
value: settings.smaller,
|
||||||
onChanged: (on) => cubit().keepSmaller(on: on),
|
onChanged: (on) => cubit().keepSmaller(on: on),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -963,6 +963,25 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testWidgets("the switches' explanations wait behind their question marks "
|
||||||
|
'(D63)', (tester) async {
|
||||||
|
await openMovement(tester);
|
||||||
|
const hint =
|
||||||
|
'When the phone counted hardly a step for a while; never while '
|
||||||
|
'recording or in a car.';
|
||||||
|
expect(find.text(hint), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(
|
||||||
|
of: find.widgetWithText(SwitchListTile, 'Remind me to move'),
|
||||||
|
matching: find.byType(HelpButton),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(hint), findsOneWidget);
|
||||||
|
expect(platform.configured, [null], reason: 'nothing switched');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('reminders turned on ask for the permissions, and the job '
|
testWidgets('reminders turned on ask for the permissions, and the job '
|
||||||
'gets the settings and the words, each change again', (tester) async {
|
'gets the settings and the words, each change again', (tester) async {
|
||||||
await openMovement(tester);
|
await openMovement(tester);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:halkyon_train_flutter/src/core/widget/help_title.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 HelpTitle(
|
||||||
|
'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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:halkyon_train_flutter/src/feature/music/data/music_downloads.dar
|
||||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_library.dart';
|
import 'package:halkyon_train_flutter/src/feature/music/data/music_library.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_store.dart';
|
import 'package:halkyon_train_flutter/src/feature/music/data/music_store.dart';
|
||||||
import 'package:halkyon_train_flutter/src/feature/music/widget/music_screen.dart';
|
import 'package:halkyon_train_flutter/src/feature/music/widget/music_screen.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
import '../../support/fake_jellyfin.dart';
|
import '../../support/fake_jellyfin.dart';
|
||||||
|
|
@ -34,7 +35,10 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: const [
|
||||||
|
...AppLocalizations.localizationsDelegates,
|
||||||
|
HalkyonUiLocalizations.delegate,
|
||||||
|
],
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
locale: const Locale('en'),
|
locale: const Locale('en'),
|
||||||
home: BlocProvider(
|
home: BlocProvider(
|
||||||
|
|
@ -112,7 +116,10 @@ void main() {
|
||||||
addTearDown(kept.dispose);
|
addTearDown(kept.dispose);
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: const [
|
||||||
|
...AppLocalizations.localizationsDelegates,
|
||||||
|
HalkyonUiLocalizations.delegate,
|
||||||
|
],
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
locale: const Locale('en'),
|
locale: const Locale('en'),
|
||||||
home: BlocProvider(
|
home: BlocProvider(
|
||||||
|
|
@ -217,7 +224,10 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: const [
|
||||||
|
...AppLocalizations.localizationsDelegates,
|
||||||
|
HalkyonUiLocalizations.delegate,
|
||||||
|
],
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
locale: const Locale('en'),
|
locale: const Locale('en'),
|
||||||
home: BlocProvider(
|
home: BlocProvider(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue