diff --git a/flutter_client/lib/theme/theme_mode_provider.dart b/flutter_client/lib/theme/theme_mode_provider.dart new file mode 100644 index 00000000..58ae0a07 --- /dev/null +++ b/flutter_client/lib/theme/theme_mode_provider.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../auth/auth_provider.dart'; + +const _kThemeModeKey = 'theme_mode'; + +enum AppThemeMode { system, dark, light } + +extension AppThemeModeMaterial on AppThemeMode { + ThemeMode get materialMode => switch (this) { + AppThemeMode.system => ThemeMode.system, + AppThemeMode.dark => ThemeMode.dark, + AppThemeMode.light => ThemeMode.light, + }; +} + +class ThemeModeController extends AsyncNotifier { + @override + Future build() async { + final storage = ref.watch(secureStorageProvider); + final raw = await storage.read(key: _kThemeModeKey); + return switch (raw) { + 'dark' => AppThemeMode.dark, + 'light' => AppThemeMode.light, + _ => AppThemeMode.system, + }; + } + + Future set(AppThemeMode mode) async { + final storage = ref.read(secureStorageProvider); + await storage.write(key: _kThemeModeKey, value: mode.name); + state = AsyncData(mode); + } +} + +final themeModeProvider = + AsyncNotifierProvider( + ThemeModeController.new, +); diff --git a/flutter_client/test/theme/theme_mode_provider_test.dart b/flutter_client/test/theme/theme_mode_provider_test.dart new file mode 100644 index 00000000..05b63da1 --- /dev/null +++ b/flutter_client/test/theme/theme_mode_provider_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'package:minstrel/auth/auth_provider.dart'; +import 'package:minstrel/theme/theme_mode_provider.dart'; + +class _MockStorage extends Mock implements FlutterSecureStorage {} + +void main() { + late _MockStorage storage; + + setUpAll(() { + registerFallbackValue(''); + }); + + setUp(() { + storage = _MockStorage(); + when(() => storage.write(key: any(named: 'key'), value: any(named: 'value'))) + .thenAnswer((_) async {}); + }); + + test('defaults to system when nothing stored', () async { + when(() => storage.read(key: 'theme_mode')).thenAnswer((_) async => null); + final container = ProviderContainer(overrides: [ + secureStorageProvider.overrideWithValue(storage), + ]); + addTearDown(container.dispose); + + final mode = await container.read(themeModeProvider.future); + expect(mode, AppThemeMode.system); + }); + + test('reads stored "dark" → AppThemeMode.dark', () async { + when(() => storage.read(key: 'theme_mode')).thenAnswer((_) async => 'dark'); + final container = ProviderContainer(overrides: [ + secureStorageProvider.overrideWithValue(storage), + ]); + addTearDown(container.dispose); + + final mode = await container.read(themeModeProvider.future); + expect(mode, AppThemeMode.dark); + }); + + test('reads stored "light" → AppThemeMode.light', () async { + when(() => storage.read(key: 'theme_mode')).thenAnswer((_) async => 'light'); + final container = ProviderContainer(overrides: [ + secureStorageProvider.overrideWithValue(storage), + ]); + addTearDown(container.dispose); + + final mode = await container.read(themeModeProvider.future); + expect(mode, AppThemeMode.light); + }); + + test('set(.light) writes "light" to storage and updates state', () async { + when(() => storage.read(key: 'theme_mode')).thenAnswer((_) async => null); + final container = ProviderContainer(overrides: [ + secureStorageProvider.overrideWithValue(storage), + ]); + addTearDown(container.dispose); + + await container.read(themeModeProvider.future); + await container.read(themeModeProvider.notifier).set(AppThemeMode.light); + + verify(() => storage.write(key: 'theme_mode', value: 'light')).called(1); + expect(container.read(themeModeProvider).value, AppThemeMode.light); + }); + + test('materialMode extension maps each enum to ThemeMode', () { + expect(AppThemeMode.system.materialMode.name, 'system'); + expect(AppThemeMode.dark.materialMode.name, 'dark'); + expect(AppThemeMode.light.materialMode.name, 'light'); + }); +}