screen-locker/stronglift_replacement/workout_app/lib/services/feedback_cues.dart
Krzysztof kuhy Rudnicki 5d0aa9b57a Add set haptics, finish chime, sound switch
dopamine-ux 05: selectionClick on recording a set or warmup,
mediumImpact + generated chime (music_theory, pinned tag) on
finish, dialog arrival on the slow motion token, AppDuration /
AppCurve in theme.dart, streak card counts up, all of it
collapsing under the OS reduced-motion flag. One Settings
"Sound" switch silences the chime and the break-end cue
(foreground service included, via a silent channel); haptics
stay. Removes the finished dopamine-ux-05 prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
2026-11-01 21:46:09 +01:00

58 lines
2.1 KiB
Dart

/// The haptic and sound cues the workout screen gives for what the user DID.
library;
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:workout_app/sandbox/sandbox_log.dart';
import 'package:workout_app/services/storage_service.dart';
/// Cues for recording a set and finishing a workout.
///
/// They reward the RECORDING, never the number recorded: a deloaded or failed
/// set and workout get exactly the confirmation a personal best does, so the
/// app cannot nudge anyone towards misreporting. Nothing here fires for a
/// deletion. Every sound has a haptic sibling that fires regardless of the
/// sound switch, because sound is the channel that fails (silenced phone,
/// noisy gym) and the haptic carries the meaning alone.
class FeedbackCues {
/// Creates cues that play on [_player].
FeedbackCues(this._player);
final AudioPlayer _player;
/// Whether sound may play. Ships on; [load] replaces it with the stored
/// choice, so a cue that fires before the read lands is still audible.
bool soundOn = true;
/// Reads the stored sound choice.
Future<void> load() async {
soundOn = await StorageService.instance.getSoundEnabled();
}
/// A set (or warmup) was just recorded: a light click, on the tap itself.
void setRecorded() {
SandboxLog.event('cue set recorded', {'haptic': 'selectionClick'});
unawaited(HapticFeedback.selectionClick());
}
/// The workout was just finished: a firmer tap and, if allowed, a chime.
Future<void> workoutFinished() async {
SandboxLog.event('cue workout finished', {
'haptic': 'mediumImpact',
'sound': soundOn,
});
unawaited(HapticFeedback.mediumImpact());
if (!soundOn) return;
await _player.play(AssetSource('sounds/workout_done.wav')).catchError((
Object error,
) {
// Never fatal, never silent: a chime that did not play must show up in
// the log, or "no sound" is indistinguishable from "no cue".
debugPrint('WorkoutApp: finish chime failed to play ($error).');
});
}
}