From 3ca038ec652ea528b873bcc3ed7b44db04c76744 Mon Sep 17 00:00:00 2001 From: airkjw Date: Sat, 13 Jun 2026 17:59:08 +0900 Subject: [PATCH] feat(settings): soundEnabled provider gates SFX and haptics Co-Authored-By: Claude Sonnet 4.6 --- lib/state/providers.dart | 7 ++++++- lib/state/sound_notifier.dart | 17 +++++++++++++++++ lib/ui/screens/game_screen.dart | 7 ++++--- test/state/sound_notifier_test.dart | 21 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 lib/state/sound_notifier.dart create mode 100644 test/state/sound_notifier_test.dart diff --git a/lib/state/providers.dart b/lib/state/providers.dart index 02e35d2..4ec04d4 100644 --- a/lib/state/providers.dart +++ b/lib/state/providers.dart @@ -11,6 +11,7 @@ import '../services/consent_service.dart'; import '../services/iap_service.dart'; import 'ads_notifier.dart'; import 'endless_best_notifier.dart'; +import 'sound_notifier.dart'; import 'game_session_notifier.dart'; import 'progress_notifier.dart'; import 'season_flow_notifier.dart'; @@ -22,8 +23,12 @@ final gameSessionProvider = GameSessionNotifier.new, ); +final soundEnabledProvider = + NotifierProvider(SoundEnabledNotifier.new); + final audioServiceProvider = Provider((ref) { - final service = AudioService(); + final service = AudioService(enabled: ref.read(soundEnabledProvider)); + ref.listen(soundEnabledProvider, (_, next) => service.enabled = next); ref.onDispose(service.dispose); return service; }); diff --git a/lib/state/sound_notifier.dart b/lib/state/sound_notifier.dart new file mode 100644 index 0000000..482b039 --- /dev/null +++ b/lib/state/sound_notifier.dart @@ -0,0 +1,17 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'providers.dart'; + +/// SFX + gameplay haptics on/off, seeded from the save repository. +class SoundEnabledNotifier extends Notifier { + @override + bool build() => ref.read(saveRepositoryProvider).soundEnabled; + + Future toggle() => set(!state); + + Future set(bool value) async { + if (state == value) return; + await ref.read(saveRepositoryProvider).setSoundEnabled(value); + state = value; + } +} diff --git a/lib/ui/screens/game_screen.dart b/lib/ui/screens/game_screen.dart index 90cf52b..dfa157f 100644 --- a/lib/ui/screens/game_screen.dart +++ b/lib/ui/screens/game_screen.dart @@ -120,16 +120,17 @@ class _GameScreenState extends ConsumerState final audio = ref.read(audioServiceProvider); if (prev?.fxTick != next.fxTick && next.lastPlacement != null) { final placement = next.lastPlacement!; + final hapticsOn = ref.read(soundEnabledProvider); if (placement.linesCleared > 0) { audio.play(placement.comboStreak >= 2 ? Sfx.combo : Sfx.clear); - HapticFeedback.mediumImpact(); + if (hapticsOn) HapticFeedback.mediumImpact(); if (placement.comboStreak >= 4) { - HapticFeedback.heavyImpact(); + if (hapticsOn) HapticFeedback.heavyImpact(); _shake.forward(from: 0); } } else { audio.play(Sfx.place); - HapticFeedback.lightImpact(); + if (hapticsOn) HapticFeedback.lightImpact(); } ref.read(tutorialProvider.notifier).onPlaced(); if (placement.linesCleared > 0) { diff --git a/test/state/sound_notifier_test.dart b/test/state/sound_notifier_test.dart new file mode 100644 index 0000000..d44499d --- /dev/null +++ b/test/state/sound_notifier_test.dart @@ -0,0 +1,21 @@ +import 'package:block_seasons/data/save_repository.dart'; +import 'package:block_seasons/state/providers.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + test('reads persisted sound flag and toggles + persists', () async { + SharedPreferences.setMockInitialValues({}); + final repo = await SaveRepository.open(); + final c = ProviderContainer( + overrides: [saveRepositoryProvider.overrideWithValue(repo)], + ); + addTearDown(c.dispose); + + expect(c.read(soundEnabledProvider), isTrue); + await c.read(soundEnabledProvider.notifier).toggle(); + expect(c.read(soundEnabledProvider), isFalse); + expect(repo.soundEnabled, isFalse); + }); +}