feat(todo): a task done ticks on phones and chimes on desktops
The developer's request of 10 Nov 2026: completing a task should feel like something. `CompletionFeedback`, held by `TodoDevice` and handed to each person's `TodoPerson`, gives a light haptic tick on Android and iOS and, on desktops, a short quiet chime of our own: two soft bell tones a fifth apart, synthesized by `apps/todo/tool/completion_sound.dart` into `assets/sounds/task_done.wav`, so it needs no one's licence. A task's check and the selection's Complete call it once, however many tasks are done; an undo is silent. Settings' Tasks turns the chime off on a desktop, kept per device in `<app support>/completion_feedback.json`; a phone is offered no switch. A device that can't play loses only the sound. The player is `audioplayers`, as Learn's sounds: on Linux it links GStreamer, so Todo's snap gains the `audio-playback` plug and GStreamer's headers to build, as Learn's recipe has. Whats-New: Completing a task now gives a light tap on phones and a short chime on computers, which Settings › Tasks turns off. Whats-New-De: Eine erledigte Aufgabe gibt jetzt auf dem Handy ein leichtes Tippen und am Computer einen kurzen Klang, den du unter Einstellungen › Aufgaben ausschaltest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1cc27ef0d6
commit
fd6647d752
22 changed files with 470 additions and 7 deletions
File diff suppressed because one or more lines are too long
BIN
apps/todo/halkyon_todo_flutter/assets/sounds/task_done.wav
Normal file
BIN
apps/todo/halkyon_todo_flutter/assets/sounds/task_done.wav
Normal file
Binary file not shown.
|
|
@ -340,6 +340,8 @@
|
|||
"weekStartLanguage": "Wie in der Sprache",
|
||||
"resetSubTasksTitle": "Unteraufgaben zurücksetzen",
|
||||
"resetSubTasksHint": "Wieder öffnen, wenn eine wiederkehrende Aufgabe erledigt ist",
|
||||
"completionSoundTitle": "Ton beim Erledigen",
|
||||
"completionSoundHint": "Ein kurzer Klang, wenn du eine Aufgabe erledigst",
|
||||
"automaticReminderTitle": "Automatische Erinnerung",
|
||||
"automaticReminderHint": "Für eine Aufgabe mit Uhrzeit, wenn sie keine eigene Erinnerung hat",
|
||||
"automaticReminderNone": "Keine",
|
||||
|
|
|
|||
|
|
@ -961,6 +961,14 @@
|
|||
"@resetSubTasksHint": {
|
||||
"description": "Settings of tasks and reminders (ADR 0033, D4)."
|
||||
},
|
||||
"completionSoundTitle": "Sound when done",
|
||||
"@completionSoundTitle": {
|
||||
"description": "Settings of tasks, on a desktop alone: whether a short chime plays as a task is marked done (decided 10 Nov 2026)."
|
||||
},
|
||||
"completionSoundHint": "A short chime as a task is marked done",
|
||||
"@completionSoundHint": {
|
||||
"description": "Explains completionSoundTitle."
|
||||
},
|
||||
"automaticReminderTitle": "Automatic reminder",
|
||||
"@automaticReminderTitle": {
|
||||
"description": "Settings of tasks and reminders (ADR 0033, D4)."
|
||||
|
|
|
|||
|
|
@ -2140,6 +2140,18 @@ abstract class AppLocalizations {
|
|||
/// **'Open them again when a recurring task is done'**
|
||||
String get resetSubTasksHint;
|
||||
|
||||
/// Settings of tasks, on a desktop alone: whether a short chime plays as a task is marked done (decided 10 Nov 2026).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sound when done'**
|
||||
String get completionSoundTitle;
|
||||
|
||||
/// Explains completionSoundTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A short chime as a task is marked done'**
|
||||
String get completionSoundHint;
|
||||
|
||||
/// Settings of tasks and reminders (ADR 0033, D4).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1306,6 +1306,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get resetSubTasksHint =>
|
||||
'Wieder öffnen, wenn eine wiederkehrende Aufgabe erledigt ist';
|
||||
|
||||
@override
|
||||
String get completionSoundTitle => 'Ton beim Erledigen';
|
||||
|
||||
@override
|
||||
String get completionSoundHint =>
|
||||
'Ein kurzer Klang, wenn du eine Aufgabe erledigst';
|
||||
|
||||
@override
|
||||
String get automaticReminderTitle => 'Automatische Erinnerung';
|
||||
|
||||
|
|
|
|||
|
|
@ -1305,6 +1305,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get resetSubTasksHint =>
|
||||
'Open them again when a recurring task is done';
|
||||
|
||||
@override
|
||||
String get completionSoundTitle => 'Sound when done';
|
||||
|
||||
@override
|
||||
String get completionSoundHint => 'A short chime as a task is marked done';
|
||||
|
||||
@override
|
||||
String get automaticReminderTitle => 'Automatic reminder';
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository
|
|||
import 'package:halkyon_todo_flutter/src/feature/section/data/sync_section_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/data/sync_settings_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/completion_feedback.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/sync_task_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/weather/data/weather_gateway.dart';
|
||||
|
|
@ -98,6 +99,10 @@ class TodoPerson(
|
|||
/// given; else for this session.
|
||||
final PaneWidths? _paneWidths,
|
||||
|
||||
/// What a task done feels like: the device's, where it is given; else
|
||||
/// nothing.
|
||||
final CompletionFeedback? _completionFeedback,
|
||||
|
||||
/// The projects whose sub-projects are folded away (decided 10 Nov
|
||||
/// 2026): the person's folder's, where it is given; else for this session.
|
||||
final FoldedProjects? _foldedProjects,
|
||||
|
|
@ -118,6 +123,10 @@ class TodoPerson(
|
|||
/// How wide the device keeps its side panes (ADR 0033, D33).
|
||||
late final PaneWidths paneWidths = _paneWidths ?? PaneWidths();
|
||||
|
||||
/// A tick or a chime as a task is done (decided 10 Nov 2026).
|
||||
late final CompletionFeedback completionFeedback =
|
||||
_completionFeedback ?? CompletionFeedback();
|
||||
|
||||
/// The projects folded in Browse and the sidebar, kept on the device.
|
||||
late final FoldedProjects foldedProjects =
|
||||
_foldedProjects ?? FoldedProjects();
|
||||
|
|
@ -364,6 +373,7 @@ class TodoDependencies({
|
|||
locationReminders: device.locationReminders,
|
||||
locationAccess: device.locationAccess,
|
||||
paneWidths: device.paneWidths,
|
||||
completionFeedback: device.completionFeedback,
|
||||
onClosed: closed.complete,
|
||||
);
|
||||
} on Object {
|
||||
|
|
@ -405,6 +415,7 @@ Future<TodoPerson> _openPersonData(
|
|||
required LocationReminderGateway locationReminders,
|
||||
required LocationReminderAccess locationAccess,
|
||||
required PaneWidths paneWidths,
|
||||
required CompletionFeedback completionFeedback,
|
||||
required void Function() onClosed,
|
||||
}) async {
|
||||
final db = await client.createSyncSession(
|
||||
|
|
@ -471,6 +482,7 @@ Future<TodoPerson> _openPersonData(
|
|||
locationReminders: locationReminders,
|
||||
locationAccess: locationAccess,
|
||||
paneWidths: paneWidths,
|
||||
completionFeedback: completionFeedback,
|
||||
foldedProjects: await FoldedProjects.open(
|
||||
File(p.join(folder.path, 'folded_projects.json')),
|
||||
),
|
||||
|
|
@ -522,6 +534,9 @@ class TodoDevice({
|
|||
|
||||
/// How wide it keeps its side panes (D33).
|
||||
required final PaneWidths paneWidths,
|
||||
|
||||
/// What a task done feels like, the sound turned off or not.
|
||||
required final CompletionFeedback completionFeedback,
|
||||
}) {
|
||||
/// Whether the device may show reminders, asked once an app start,
|
||||
/// whoever is signed in (D31).
|
||||
|
|
@ -542,6 +557,9 @@ class TodoDevice({
|
|||
appBuild: int.tryParse(package.buildNumber),
|
||||
appVersion: package.version,
|
||||
paneWidths: await PaneWidths.open(File(p.join(support, 'panes.json'))),
|
||||
completionFeedback: await CompletionFeedback.forDevice(
|
||||
File(p.join(support, 'completion_feedback.json')),
|
||||
),
|
||||
reminders: await _reminders(),
|
||||
locationReminders: Platform.isAndroid
|
||||
? const AndroidLocationReminders()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// What a task done feels like (decided by the developer, 10 Nov 2026): a
|
||||
/// light haptic tick on a phone, a short quiet chime of Todo's own on a
|
||||
/// desktop ([sounds]), which Settings turns off there. The device's matter,
|
||||
/// whoever is signed in, kept in [_file] where there is one; neither sealed
|
||||
/// nor synced.
|
||||
class CompletionFeedback({
|
||||
final File? _file,
|
||||
|
||||
/// Whether this device chimes rather than ticks: a desktop's.
|
||||
final bool sounds = false,
|
||||
|
||||
/// The tick; nothing in tests that don't say.
|
||||
final Future<void> Function() _tick = _nothing,
|
||||
|
||||
/// The chime; nothing in tests that don't say.
|
||||
final Future<void> Function() _chime = _nothing,
|
||||
}) extends ChangeNotifier {
|
||||
var _soundOn = true;
|
||||
|
||||
/// Opens what [file] holds; the sound on where it can't be read.
|
||||
static Future<CompletionFeedback> open(
|
||||
File file, {
|
||||
required bool sounds,
|
||||
Future<void> Function() tick = _nothing,
|
||||
Future<void> Function() chime = _nothing,
|
||||
}) async {
|
||||
final feedback = CompletionFeedback(
|
||||
file: file,
|
||||
sounds: sounds,
|
||||
tick: tick,
|
||||
chime: chime,
|
||||
);
|
||||
try {
|
||||
if (file.existsSync()) {
|
||||
final json = jsonDecode(await file.readAsString());
|
||||
if (json is Map<String, Object?> && json['sound'] is bool) {
|
||||
feedback._soundOn = json['sound']! as bool;
|
||||
}
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('Completion sound not read: ${error.runtimeType}', name: 'Tasks');
|
||||
}
|
||||
return feedback;
|
||||
}
|
||||
|
||||
/// The device's own: a tick on Android and iOS, else the chime from
|
||||
/// `assets/sounds/task_done.wav`.
|
||||
static Future<CompletionFeedback> forDevice(File file) {
|
||||
final phone = Platform.isAndroid || Platform.isIOS;
|
||||
return open(
|
||||
file,
|
||||
sounds: !phone,
|
||||
tick: HapticFeedback.lightImpact,
|
||||
chime: phone ? _nothing : _AssetChime().play,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether Settings offers turning the sound off: on a desktop alone.
|
||||
bool get offersSound => sounds;
|
||||
|
||||
/// Whether a desktop chimes; on at first.
|
||||
bool get soundOn => _soundOn;
|
||||
|
||||
/// Turns the chime [on] or off, and keeps it.
|
||||
Future<void> setSoundOn({required bool on}) async {
|
||||
if (_soundOn == on) {
|
||||
return;
|
||||
}
|
||||
_soundOn = on;
|
||||
notifyListeners();
|
||||
try {
|
||||
await _file?.writeAsString(jsonEncode({'sound': on}));
|
||||
} on Object catch (error) {
|
||||
log('Completion sound not kept: ${error.runtimeType}', name: 'Tasks');
|
||||
}
|
||||
}
|
||||
|
||||
/// A task, or several at once, was just marked done: once whatever their
|
||||
/// number. Never throws: a device that can't play loses only the sound.
|
||||
Future<void> completed() async {
|
||||
try {
|
||||
if (!sounds) {
|
||||
await _tick();
|
||||
} else if (_soundOn) {
|
||||
await _chime();
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('No completion feedback: ${error.runtimeType}', name: 'Tasks');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _nothing() async {}
|
||||
|
||||
/// The chime on `audioplayers`, one player made at the first, kept for the
|
||||
/// app's life.
|
||||
class _AssetChime() {
|
||||
late final _player = AudioPlayer();
|
||||
|
||||
Future<void> play() async {
|
||||
await _player.stop();
|
||||
await _player.play(AssetSource('sounds/task_done.wav'));
|
||||
}
|
||||
}
|
||||
|
|
@ -355,6 +355,12 @@ class _SelectionActions(
|
|||
final context = _context;
|
||||
final tasks = _tasks;
|
||||
final chosen = _chosen.where((task) => !task.content.completed).toList();
|
||||
if (chosen.isNotEmpty) {
|
||||
// Once for them all.
|
||||
unawaited(
|
||||
PersonScope.of<TodoPerson>(context).completionFeedback.completed(),
|
||||
);
|
||||
}
|
||||
final reopen = context.read<TodoCubit>().state.settings.tasks.resetSubTasks;
|
||||
final now = clock.now();
|
||||
final before = await _write(
|
||||
|
|
|
|||
|
|
@ -386,9 +386,14 @@ class const TaskCheck({required final OpenTask task, super.key})
|
|||
color: color,
|
||||
),
|
||||
onPressed: () async {
|
||||
final tasks = PersonScope.of<TodoPerson>(context).tasks;
|
||||
final person = PersonScope.of<TodoPerson>(context);
|
||||
final tasks = person.tasks;
|
||||
final stored = StoredTask(task.id, content);
|
||||
final done = !content.completed;
|
||||
if (done) {
|
||||
// Felt as the tap lands, not once it is written.
|
||||
unawaited(person.completionFeedback.completed());
|
||||
}
|
||||
final reopen = context
|
||||
.read<TodoCubit>()
|
||||
.state
|
||||
|
|
|
|||
|
|
@ -75,11 +75,37 @@ class const TaskSettingsCard({super.key}) extends StatelessWidget {
|
|||
settings.copyWith(tasks: tasks.copyWith(resetSubTasks: on)),
|
||||
),
|
||||
),
|
||||
const _CompletionSound(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a desktop chimes as a task is done (decided 10 Nov 2026): the
|
||||
/// device's, whoever is signed in; nothing on a phone, which ticks.
|
||||
class const _CompletionSound() extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final feedback = PersonScope.of<TodoPerson>(context).completionFeedback;
|
||||
if (!feedback.offersSound) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ListenableBuilder(
|
||||
listenable: feedback,
|
||||
builder: (context, _) => SwitchListTile(
|
||||
secondary: const Icon(LucideIcons.volume2),
|
||||
title: ExplainedTitle(
|
||||
l10n.completionSoundTitle,
|
||||
help: l10n.completionSoundHint,
|
||||
),
|
||||
value: feedback.soundOn,
|
||||
onChanged: (on) => feedback.setSoundOn(on: on),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings' *Reminders* (D4): the automatic reminder a task given a time
|
||||
/// gets where it has none of its own (none at first), and how long a
|
||||
/// reminder is snoozed for from its notification; above them, while
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
|
|
@ -14,6 +15,9 @@
|
|||
#include <window_to_front/window_to_front_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
|
||||
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin");
|
||||
desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_linux
|
||||
desktop_webview_window
|
||||
emoji_picker_flutter
|
||||
file_selector_linux
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import audioplayers_darwin
|
||||
import connectivity_plus
|
||||
import desktop_webview_window
|
||||
import device_info_plus
|
||||
|
|
@ -23,6 +24,7 @@ import url_launcher_macos
|
|||
import window_to_front
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ environment:
|
|||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
# The chime of a task done on a desktop (decided 10 Nov 2026), as Learn's
|
||||
# sounds: GStreamer on Linux.
|
||||
audioplayers: ^6.8.1
|
||||
clock: ^1.1.3
|
||||
# Reactions on comments (ADR 0033, D36): every emoji, searched, recents on
|
||||
# the device.
|
||||
|
|
@ -101,6 +104,9 @@ flutter:
|
|||
# (<version>.<language>.txt), drafted by tool/release.dart and edited
|
||||
# (Train's ADR 0030, D61): halkyon_app's What's new shows them.
|
||||
- assets/whats_new/
|
||||
# The chime of a task done on a desktop, Todo's own, synthesized by
|
||||
# ../tool/completion_sound.dart.
|
||||
- assets/sounds/task_done.wav
|
||||
# The app's icon, Lucide's list-todo (ADR 0031), and Lucide's licence for the
|
||||
# licence list (the repository's tool/app_icons.py writes both).
|
||||
- assets/icon/halkyon_todo_icon.png
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository
|
|||
import 'package:halkyon_todo_flutter/src/feature/section/model/section.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/completion_feedback.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
|
|
@ -99,6 +100,12 @@ void main() {
|
|||
/// The projects folded in Browse and the sidebar.
|
||||
late FoldedProjects folded;
|
||||
|
||||
/// What the device did as tasks were done: `tick` or `chime`, in order.
|
||||
late List<String> felt;
|
||||
|
||||
/// What a task done feels like: a phone's unless a test says.
|
||||
late CompletionFeedback completion;
|
||||
|
||||
/// How many syncs ran.
|
||||
late int syncs;
|
||||
|
||||
|
|
@ -133,6 +140,11 @@ void main() {
|
|||
locationGateway = MemoryLocationGateway();
|
||||
speech = FakeSpeechInput();
|
||||
folded = FoldedProjects();
|
||||
felt = [];
|
||||
completion = CompletionFeedback(
|
||||
tick: () async => felt.add('tick'),
|
||||
chime: () async => felt.add('chime'),
|
||||
);
|
||||
store = MemoryInstanceStore();
|
||||
looks = MemoryLookStore();
|
||||
gateway = FakeAuthGateway();
|
||||
|
|
@ -239,6 +251,7 @@ void main() {
|
|||
locationReminders: locationGateway,
|
||||
calendarFeeds: calendarFeeds,
|
||||
foldedProjects: folded,
|
||||
completionFeedback: completion,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -3917,13 +3930,16 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('Water plants'), findsNothing);
|
||||
expect(find.text('Task done'), findsOneWidget);
|
||||
expect(felt, ['tick'], reason: "a phone's tick");
|
||||
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Water plants'), findsOneWidget);
|
||||
expect(felt, ['tick'], reason: 'nothing on an undo');
|
||||
|
||||
await tester.tap(find.byTooltip('Mark as done'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(felt, ['tick', 'tick']);
|
||||
await tester.tap(find.byTooltip('Display'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Completed tasks'));
|
||||
|
|
@ -6023,6 +6039,7 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('Water plants'), findsNothing);
|
||||
expect(find.text('2 tasks done'), findsOneWidget);
|
||||
expect(felt, ['tick'], reason: 'once for both');
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Water plants'), findsOneWidget);
|
||||
|
|
@ -6708,6 +6725,58 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("a desktop's chime of a task done is turned off in Tasks, "
|
||||
'for the device; a phone, which ticks, is offered none', (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await openSettings(tester);
|
||||
expect(find.text('Sound when done'), findsNothing);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
completion = CompletionFeedback(
|
||||
sounds: true,
|
||||
tick: () async => felt.add('tick'),
|
||||
chime: () async => felt.add('chime'),
|
||||
);
|
||||
await start(tester, usedBefore: true);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(title: 'Water plants', projectId: inbox()),
|
||||
);
|
||||
await goTab(tester, 'Inbox');
|
||||
await tester.tap(find.byTooltip('Mark as done'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(felt, ['chime']);
|
||||
// The undo's snack bar gone first, as it would be by the time.
|
||||
ScaffoldMessenger.of(
|
||||
tester.element(find.byType(SnackBar)),
|
||||
).removeCurrentSnackBar();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await openSettings(tester);
|
||||
await tapInSettings(tester, 'Sound when done');
|
||||
expect(completion.soundOn, isFalse);
|
||||
expect(
|
||||
tester
|
||||
.widget<SwitchListTile>(
|
||||
find.widgetWithText(SwitchListTile, 'Sound when done'),
|
||||
)
|
||||
.value,
|
||||
isFalse,
|
||||
);
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(title: 'Post letter', projectId: inbox()),
|
||||
);
|
||||
await goTab(tester, 'Inbox');
|
||||
await tester.tap(find.byTooltip('Mark as done'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(felt, ['chime'], reason: 'quiet now');
|
||||
});
|
||||
|
||||
testWidgets('a task added with a time gets the automatic reminder; '
|
||||
'reminders are snoozed for as long as set', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/completion_feedback.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
late int ticks;
|
||||
late int chimes;
|
||||
|
||||
setUp(() {
|
||||
ticks = 0;
|
||||
chimes = 0;
|
||||
});
|
||||
|
||||
Future<void> tick() async => ticks++;
|
||||
Future<void> chime() async => chimes++;
|
||||
|
||||
test('a phone ticks once a task is done, and offers no sound', () async {
|
||||
final feedback = CompletionFeedback(tick: tick, chime: chime);
|
||||
|
||||
await feedback.completed();
|
||||
|
||||
expect(feedback.offersSound, isFalse);
|
||||
expect((ticks, chimes), (1, 0));
|
||||
});
|
||||
|
||||
test('a desktop chimes, while its sound is on, and never ticks', () async {
|
||||
final feedback = CompletionFeedback(
|
||||
sounds: true,
|
||||
tick: tick,
|
||||
chime: chime,
|
||||
);
|
||||
expect(feedback.offersSound, isTrue);
|
||||
expect(feedback.soundOn, isTrue, reason: 'on at first');
|
||||
|
||||
await feedback.completed();
|
||||
expect((ticks, chimes), (0, 1));
|
||||
|
||||
await feedback.setSoundOn(on: false);
|
||||
await feedback.completed();
|
||||
expect((ticks, chimes), (0, 1));
|
||||
});
|
||||
|
||||
test("a sound that can't play takes nothing with it", () async {
|
||||
final feedback = CompletionFeedback(
|
||||
sounds: true,
|
||||
chime: () async => throw StateError('no GStreamer'),
|
||||
);
|
||||
|
||||
await expectLater(feedback.completed(), completes);
|
||||
});
|
||||
|
||||
group('kept on the device', () {
|
||||
late Directory folder;
|
||||
|
||||
setUp(() async {
|
||||
folder = await Directory.systemTemp.createTemp('completion');
|
||||
addTearDown(() => folder.delete(recursive: true));
|
||||
});
|
||||
|
||||
test('the sound turned off stays off when the app opens again', () async {
|
||||
final file = File(p.join(folder.path, 'completion_feedback.json'));
|
||||
final feedback = await CompletionFeedback.open(file, sounds: true);
|
||||
var changes = 0;
|
||||
feedback.addListener(() => changes++);
|
||||
|
||||
await feedback.setSoundOn(on: false);
|
||||
expect(changes, 1);
|
||||
|
||||
final again = await CompletionFeedback.open(file, sounds: true);
|
||||
expect(again.soundOn, isFalse);
|
||||
});
|
||||
|
||||
test('a file that is no setting leaves the sound on', () async {
|
||||
final file = File(p.join(folder.path, 'completion_feedback.json'));
|
||||
await file.writeAsString('not json');
|
||||
expect((await CompletionFeedback.open(file, sounds: true)).soundOn, true);
|
||||
await file.writeAsString('[1]');
|
||||
expect((await CompletionFeedback.open(file, sounds: true)).soundOn, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
|
||||
|
|
@ -16,6 +17,8 @@
|
|||
#include <window_to_front/window_to_front_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AudioplayersWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_windows
|
||||
connectivity_plus
|
||||
desktop_webview_window
|
||||
emoji_picker_flutter
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue