feat(app): choose whether studying shows the Pomodoro timer
Settings has a Study card: the Pomodoro profiles, and a switch "Show the Pomodoro timer while studying" (on unless switched off). Like the appearance, it is the device's own setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
aad6110da3
commit
b72a96f810
14 changed files with 216 additions and 12 deletions
|
|
@ -287,6 +287,9 @@
|
|||
"settingsCitationStyleChicago": "Chicago (18. Auflage)",
|
||||
"settingsCitationPreview": "Vorschau",
|
||||
"settingsCitationGuide": "Offizieller Leitfaden",
|
||||
"settingsStudy": "Lernen",
|
||||
"settingsStudyPomodoroTitle": "Pomodoro-Timer beim Lernen anzeigen",
|
||||
"settingsStudyPomodoroSubtitle": "Zeige den Fokus-/Pausen-Countdown in der Lernsitzung, damit du die verbleibende Zeit siehst, ohne die Benachrichtigung zu öffnen.",
|
||||
"settingsVersion": "Version {version} (Build {build})",
|
||||
"syncTitle": "Synchronisierung",
|
||||
"syncStateSyncing": "Wird synchronisiert…",
|
||||
|
|
|
|||
|
|
@ -312,6 +312,9 @@
|
|||
"settingsCitationStyleChicago": "Chicago (18th edition)",
|
||||
"settingsCitationPreview": "Preview",
|
||||
"settingsCitationGuide": "Official guide",
|
||||
"settingsStudy": "Study",
|
||||
"settingsStudyPomodoroTitle": "Show the Pomodoro timer while studying",
|
||||
"settingsStudyPomodoroSubtitle": "Show the focus / break countdown inside the study session, so you can see the time left without opening the notification.",
|
||||
"settingsVersion": "Version {version} (build {build})",
|
||||
"@settingsVersion": {"placeholders": {"version": {"type": "String"}, "build": {"type": "String"}}},
|
||||
"syncTitle": "Sync",
|
||||
|
|
|
|||
|
|
@ -1762,6 +1762,24 @@ abstract class AppLocalizations {
|
|||
/// **'Official guide'**
|
||||
String get settingsCitationGuide;
|
||||
|
||||
/// No description provided for @settingsStudy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Study'**
|
||||
String get settingsStudy;
|
||||
|
||||
/// No description provided for @settingsStudyPomodoroTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show the Pomodoro timer while studying'**
|
||||
String get settingsStudyPomodoroTitle;
|
||||
|
||||
/// No description provided for @settingsStudyPomodoroSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show the focus / break countdown inside the study session, so you can see the time left without opening the notification.'**
|
||||
String get settingsStudyPomodoroSubtitle;
|
||||
|
||||
/// No description provided for @settingsVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1009,6 +1009,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settingsCitationGuide => 'Offizieller Leitfaden';
|
||||
|
||||
@override
|
||||
String get settingsStudy => 'Lernen';
|
||||
|
||||
@override
|
||||
String get settingsStudyPomodoroTitle =>
|
||||
'Pomodoro-Timer beim Lernen anzeigen';
|
||||
|
||||
@override
|
||||
String get settingsStudyPomodoroSubtitle =>
|
||||
'Zeige den Fokus-/Pausen-Countdown in der Lernsitzung, damit du die verbleibende Zeit siehst, ohne die Benachrichtigung zu öffnen.';
|
||||
|
||||
@override
|
||||
String settingsVersion(String version, String build) {
|
||||
return 'Version $version (Build $build)';
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get settingsCitationGuide => 'Official guide';
|
||||
|
||||
@override
|
||||
String get settingsStudy => 'Study';
|
||||
|
||||
@override
|
||||
String get settingsStudyPomodoroTitle =>
|
||||
'Show the Pomodoro timer while studying';
|
||||
|
||||
@override
|
||||
String get settingsStudyPomodoroSubtitle =>
|
||||
'Show the focus / break countdown inside the study session, so you can see the time left without opening the notification.';
|
||||
|
||||
@override
|
||||
String settingsVersion(String version, String build) {
|
||||
return 'Version $version (build $build)';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ class DeviceSettingsBloc(
|
|||
on<DeviceSettingsEventCitationStyleChosen>(
|
||||
(event, emit) => _apply(emit, state.withCitationStyle(event.style)),
|
||||
);
|
||||
on<DeviceSettingsEventPomodoroWhileStudyingChosen>(
|
||||
(event, emit) =>
|
||||
_apply(emit, state.withPomodoroWhileStudying(event.shown)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _apply(
|
||||
|
|
@ -65,3 +69,11 @@ final class const DeviceSettingsEventCitationStyleChosen(
|
|||
@override
|
||||
List<Object?> get props => [style];
|
||||
}
|
||||
|
||||
/// The person chose whether studying shows the Pomodoro timer.
|
||||
final class const DeviceSettingsEventPomodoroWhileStudyingChosen(
|
||||
final bool shown,
|
||||
) extends DeviceSettingsEvent {
|
||||
@override
|
||||
List<Object?> get props => [shown];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
|||
citationStyle:
|
||||
CitationStyle.values.asNameMap()[json['citationStyle']] ??
|
||||
CitationStyle.apa,
|
||||
pomodoroWhileStudying: json['pomodoroWhileStudying'] != false,
|
||||
);
|
||||
} on FormatException catch (error) {
|
||||
log('The device settings are unreadable', name: 'Settings', error: error);
|
||||
|
|
@ -62,6 +63,7 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
|||
'appearance': settings.appearance.name,
|
||||
'language': ?settings.language,
|
||||
'citationStyle': settings.citationStyle.name,
|
||||
'pomodoroWhileStudying': settings.pomodoroWhileStudying,
|
||||
}),
|
||||
flush: true,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ enum Appearance() {
|
|||
}
|
||||
|
||||
/// The settings of one device, which stay on it (decided 2026-10-07): how the
|
||||
/// app looks, the language it speaks and the style it cites sources in.
|
||||
/// app looks, the language it speaks, the style it cites sources in, and
|
||||
/// whether studying shows the Pomodoro timer.
|
||||
/// Unlike the person's settings, they don't follow anyone to other devices,
|
||||
/// and they apply before anyone is signed in.
|
||||
final class const DeviceSettings({
|
||||
|
|
@ -21,12 +22,16 @@ final class const DeviceSettings({
|
|||
|
||||
/// How sources and bibliographies are cited.
|
||||
final CitationStyle citationStyle = CitationStyle.apa,
|
||||
|
||||
/// Whether a study session shows the Pomodoro timer's countdown.
|
||||
final bool pomodoroWhileStudying = true,
|
||||
}) with Equatable {
|
||||
/// These settings with [appearance] instead.
|
||||
DeviceSettings withAppearance(Appearance appearance) => DeviceSettings(
|
||||
appearance: appearance,
|
||||
language: language,
|
||||
citationStyle: citationStyle,
|
||||
pomodoroWhileStudying: pomodoroWhileStudying,
|
||||
);
|
||||
|
||||
/// These settings with [language] instead; null for the device's.
|
||||
|
|
@ -34,6 +39,7 @@ final class const DeviceSettings({
|
|||
appearance: appearance,
|
||||
language: language,
|
||||
citationStyle: citationStyle,
|
||||
pomodoroWhileStudying: pomodoroWhileStudying,
|
||||
);
|
||||
|
||||
/// These settings with [citationStyle] instead.
|
||||
|
|
@ -42,8 +48,22 @@ final class const DeviceSettings({
|
|||
appearance: appearance,
|
||||
language: language,
|
||||
citationStyle: citationStyle,
|
||||
pomodoroWhileStudying: pomodoroWhileStudying,
|
||||
);
|
||||
|
||||
/// These settings with [shown] for the Pomodoro timer while studying.
|
||||
DeviceSettings withPomodoroWhileStudying(bool shown) => DeviceSettings(
|
||||
appearance: appearance,
|
||||
language: language,
|
||||
citationStyle: citationStyle,
|
||||
pomodoroWhileStudying: shown,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [appearance, language, citationStyle];
|
||||
List<Object?> get props => [
|
||||
appearance,
|
||||
language,
|
||||
citationStyle,
|
||||
pomodoroWhileStudying,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/bloc/pomodoro_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_card.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -7,12 +8,16 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// The Pomodoro timer, small, while studying: the time left of its phase
|
||||
/// beside a timer (or a pause while paused), in the primary colour while
|
||||
/// focusing and teal on a break. Only to watch: the timer is worked from its
|
||||
/// card and its notification. Nothing while no timer is set.
|
||||
/// card and its notification. Nothing while no timer is set, or where the
|
||||
/// device's settings leave it out of studying.
|
||||
class const PomodoroChip({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final run = context.watch<PomodoroBloc>().state.run;
|
||||
if (run == null) {
|
||||
final shown = context.select<DeviceSettingsBloc, bool>(
|
||||
(bloc) => bloc.state.pomodoroWhileStudying,
|
||||
);
|
||||
if (run == null || !shown) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/layout/readable_width.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/sync/widget/sync_status_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/confirm_dialog.dart';
|
||||
|
|
@ -39,14 +40,7 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
|
|||
const SizedBox(height: 12),
|
||||
const CitationSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: const Icon(LucideIcons.timer),
|
||||
title: Text(l10n.pomodoroProfiles),
|
||||
onTap: () => showPomodoroProfilesSheet(context),
|
||||
),
|
||||
),
|
||||
const _StudyCard(),
|
||||
const SizedBox(height: 12),
|
||||
SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
|
|
@ -154,3 +148,44 @@ Future<void> _delete(BuildContext context) async {
|
|||
session.add(const SessionEventDeleteRequested());
|
||||
}
|
||||
}
|
||||
|
||||
/// How studying goes on this device: the Pomodoro profiles, and whether a
|
||||
/// session shows the timer.
|
||||
class const _StudyCard() extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return SectionCard(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settingsStudy,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.timer),
|
||||
title: Text(l10n.pomodoroProfiles),
|
||||
onTap: () => showPomodoroProfilesSheet(context),
|
||||
),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(l10n.settingsStudyPomodoroTitle),
|
||||
subtitle: Text(l10n.settingsStudyPomodoroSubtitle),
|
||||
value: context.select<DeviceSettingsBloc, bool>(
|
||||
(bloc) => bloc.state.pomodoroWhileStudying,
|
||||
),
|
||||
onChanged: (shown) => context.read<DeviceSettingsBloc>().add(
|
||||
DeviceSettingsEventPomodoroWhileStudyingChosen(shown),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,4 +76,16 @@ void main() {
|
|||
await pumpEventQueue();
|
||||
expect(store.writes, [after]);
|
||||
});
|
||||
|
||||
test('the Pomodoro timer can be hidden while studying', () async {
|
||||
final store = MemoryDeviceSettingsStore();
|
||||
final bloc = DeviceSettingsBloc(store, const DeviceSettings())
|
||||
..add(const DeviceSettingsEventPomodoroWhileStudyingChosen(false));
|
||||
addTearDown(bloc.close);
|
||||
|
||||
const after = DeviceSettings(pomodoroWhileStudying: false);
|
||||
await next(bloc, after);
|
||||
await pumpEventQueue();
|
||||
expect(store.writes, [after]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,17 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('shows the Pomodoro timer while studying until told not to', () async {
|
||||
expect((await store.read()).pomodoroWhileStudying, isTrue);
|
||||
|
||||
await store.write(const DeviceSettings(pomodoroWhileStudying: false));
|
||||
|
||||
expect(
|
||||
(await FileDeviceSettingsStore(file).read()).pomodoroWhileStudying,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps a return to the device language', () async {
|
||||
await store.write(const DeviceSettings(language: 'en-US'));
|
||||
await store.write(const DeviceSettings(appearance: Appearance.light));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
|
|
@ -264,4 +265,34 @@ void main() {
|
|||
await tester.pump();
|
||||
expect(platform.showing?.isRunning, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('the timer stays out of studying where the device says so', (
|
||||
tester,
|
||||
) async {
|
||||
final now = clock.now().toUtc();
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
pomodoro: platform,
|
||||
deviceSettings: const DeviceSettings(pomodoroWhileStudying: false),
|
||||
studysetGateway: FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
pomodoroRuns: MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(PomodoroProfile.classic).started(now),
|
||||
),
|
||||
),
|
||||
location: RoutePaths.study('japanese'),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(PomodoroChip),
|
||||
matching: find.textContaining(':'),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,36 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets('the Study card hides the Pomodoro timer while studying, and '
|
||||
'opens the Pomodoro profiles', (tester) async {
|
||||
final store = MemoryDeviceSettingsStore();
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(deviceSettingsStore: store),
|
||||
location: RoutePaths.settings,
|
||||
);
|
||||
final timer = find.text('Show the Pomodoro timer while studying');
|
||||
await tester.scrollUntilVisible(
|
||||
timer,
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('Pomodoro profiles'), findsOneWidget);
|
||||
|
||||
final toggle = find.widgetWithText(
|
||||
SwitchListTile,
|
||||
'Show the Pomodoro timer while studying',
|
||||
);
|
||||
await tester.ensureVisible(toggle);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(of: toggle, matching: find.byType(Switch)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(store.writes.last.pomodoroWhileStudying, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('starts with the appearance the device kept', (tester) async {
|
||||
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
|
||||
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
|
||||
|
|
|
|||
Loading…
Reference in a new issue