diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ae9c74f..542a1e24 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,6 @@ "README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md", "*.dart": "${capture}.g.dart,${capture}.freezed.dart" }, - "dart.flutterSdkPath": ".fvm/versions/3.29.2" + "dart.flutterSdkPath": ".fvm/versions/3.29.2", + "makefile.configureOnOpen": false } \ No newline at end of file diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 543bc471..aea9dded 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -30,7 +30,7 @@ class AppRouter extends RootStackRouter { (resolver, router) async { final auth = await ref.read(authenticationProvider.future); - if (auth == null && !KVStoreService.doneGettingStarted) { + if (auth == null && !KVStoreService().doneGettingStarted) { resolver.redirect(const GettingStartedRoute()); } else { resolver.next(true); diff --git a/lib/collections/vars.dart b/lib/collections/vars.dart new file mode 100644 index 00000000..c308b887 --- /dev/null +++ b/lib/collections/vars.dart @@ -0,0 +1,3 @@ +import 'package:get_it/get_it.dart'; + +final getIt = GetIt.instance; diff --git a/lib/hooks/configurators/use_check_yt_dlp_installed.dart b/lib/hooks/configurators/use_check_yt_dlp_installed.dart index 1d948258..7339809b 100644 --- a/lib/hooks/configurators/use_check_yt_dlp_installed.dart +++ b/lib/hooks/configurators/use_check_yt_dlp_installed.dart @@ -21,7 +21,7 @@ void useCheckYtDlpInstalled(WidgetRef ref) { ); final customPath = - KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp); + KVStoreService().getYoutubeEnginePath(YoutubeClientEngine.ytDlp); if (youtubeEngine == YoutubeClientEngine.ytDlp && !await YtDlpEngine.isInstalled() && diff --git a/lib/hooks/configurators/use_disable_battery_optimizations.dart b/lib/hooks/configurators/use_disable_battery_optimizations.dart index 4aa51b74..eea9786d 100644 --- a/lib/hooks/configurators/use_disable_battery_optimizations.dart +++ b/lib/hooks/configurators/use_disable_battery_optimizations.dart @@ -6,7 +6,7 @@ import 'package:spotube/utils/platform.dart'; void useDisableBatteryOptimizations() { useAsyncEffect(() async { - if (!kIsAndroid || KVStoreService.askedForBatteryOptimization) return; + if (!kIsAndroid || KVStoreService().askedForBatteryOptimization) return; await DisableBatteryOptimization.showDisableBatteryOptimizationSettings(); @@ -16,6 +16,6 @@ void useDisableBatteryOptimizations() { "Follow the steps and disable the optimizations to allow smooth functioning of this app", ); - await KVStoreService.setAskedForBatteryOptimization(true); + await KVStoreService().setAskedForBatteryOptimization(true); }, null, []); } diff --git a/lib/main.dart b/lib/main.dart index f178b663..1c933547 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,12 +8,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:get_it/get_it.dart'; import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:smtc_windows/smtc_windows.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/initializers.dart'; @@ -65,6 +67,14 @@ Future main(List rawArgs) async { AppLogger.runZoned(() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + GetIt.I.registerSingleton( + await SharedPreferences.getInstance(), + ); + GetIt.I.registerSingletonWithDependencies( + () => KVStoreService.init(), + dependsOn: [SharedPreferences], + ); + await registerWindowsScheme("spotify"); tz.initializeTimeZones(); @@ -85,13 +95,11 @@ Future main(List rawArgs) async { MetadataGod.initialize(); } - await KVStoreService.initialize(); - if (kIsDesktop) { await windowManager.setPreventClose(true); await YtDlp.instance .setBinaryLocation( - KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ?? + KVStoreService().getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ?? "yt-dlp${kIsWindows ? '.exe' : ''}", ) .catchError((e, stack) => null); diff --git a/lib/models/database/typeconverters/encrypted_text.dart b/lib/models/database/typeconverters/encrypted_text.dart index 6afa8210..bbe51ccd 100644 --- a/lib/models/database/typeconverters/encrypted_text.dart +++ b/lib/models/database/typeconverters/encrypted_text.dart @@ -16,7 +16,7 @@ class DecryptedText { return DecryptedText( _encrypter!.decrypt( Encrypted.fromBase64(value), - iv: KVStoreService.ivKey, + iv: KVStoreService().ivKey, ), ); } @@ -27,7 +27,7 @@ class DecryptedText { Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync), ), ); - return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64; + return _encrypter!.encrypt(value, iv: KVStoreService().ivKey).base64; } } diff --git a/lib/modules/settings/youtube_engine_not_installed_dialog.dart b/lib/modules/settings/youtube_engine_not_installed_dialog.dart index b993dd1b..aaf1cf6b 100644 --- a/lib/modules/settings/youtube_engine_not_installed_dialog.dart +++ b/lib/modules/settings/youtube_engine_not_installed_dialog.dart @@ -103,7 +103,7 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget { ?.invalidate(context.l10n.file_not_found); return; } - await KVStoreService.setYoutubeEnginePath( + await KVStoreService().setYoutubeEnginePath( engine, controller.text, ); diff --git a/lib/pages/getting_started/sections/support.dart b/lib/pages/getting_started/sections/support.dart index 9559d28d..f416349e 100644 --- a/lib/pages/getting_started/sections/support.dart +++ b/lib/pages/getting_started/sections/support.dart @@ -110,7 +110,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { Button.secondary( leading: const Icon(SpotubeIcons.anonymous), onPressed: () async { - await KVStoreService.setDoneGettingStarted(true); + await KVStoreService().setDoneGettingStarted(true); if (context.mounted) { context.navigateTo(const HomeRoute()); } @@ -134,7 +134,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { }, ), onPressed: () async { - await KVStoreService.setDoneGettingStarted(true); + await KVStoreService().setDoneGettingStarted(true); await onLogin(); }, child: Text( diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index eeedfb9c..27b9cb25 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -62,10 +62,10 @@ class SearchPage extends HookConsumerWidget { if (value.trim().isEmpty) { return; } - KVStoreService.setRecentSearches( + KVStoreService().setRecentSearches( { value, - ...KVStoreService.recentSearches, + ...KVStoreService().recentSearches, }.toList(), ); } @@ -96,8 +96,9 @@ class SearchPage extends HookConsumerWidget { listenable: controller, builder: (context, _) { final suggestions = controller.text.isEmpty - ? KVStoreService.recentSearches - : KVStoreService.recentSearches + ? KVStoreService().recentSearches + : KVStoreService() + .recentSearches .where( (s) => weightedRatio( diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 54273904..bad7745b 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -410,7 +410,8 @@ class SettingsPlaybackSection extends HookConsumerWidget { onChanged: (value) async { if (value == null) return; if (value == YoutubeClientEngine.ytDlp) { - final customPath = KVStoreService.getYoutubeEnginePath(value); + final customPath = + KVStoreService().getYoutubeEnginePath(value); if (!await YtDlpEngine.isInstalled() && (customPath == null || !await File(customPath).exists()) && diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 923e733a..68f937aa 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -8,6 +8,8 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/user_preferences/default_download_dir_provider.dart'; +import 'package:spotube/provider/window_manager/window_manager.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/sourced_track/enums.dart'; diff --git a/lib/provider/volume_provider.dart b/lib/provider/volume_provider.dart index 64bcfe1a..969df8c8 100644 --- a/lib/provider/volume_provider.dart +++ b/lib/provider/volume_provider.dart @@ -5,20 +5,25 @@ import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; class VolumeProvider extends Notifier { - VolumeProvider(); + final SpotubeAudioPlayer _audioPlayer; + + VolumeProvider({ + required SpotubeAudioPlayer audioPlayer, + }) : _audioPlayer = audioPlayer; @override build() { - audioPlayer.setVolume(KVStoreService.volume); - return KVStoreService.volume; + _audioPlayer.setVolume(KVStoreService().volume); + return KVStoreService().volume; } Future setVolume(double volume) async { state = volume; - await audioPlayer.setVolume(volume); - KVStoreService.setVolume(volume); + await _audioPlayer.setVolume(volume); + KVStoreService().setVolume(volume); } } -final volumeProvider = - NotifierProvider(() => VolumeProvider()); +final volumeProvider = NotifierProvider(() { + return VolumeProvider(audioPlayer: audioPlayer); +}); diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart index 4eca0007..e2fd2bc1 100644 --- a/lib/services/kv_store/encrypted_kv_store.dart +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -25,7 +25,7 @@ abstract class EncryptedKvStoreService { static Future get encryptionKey async { if (isUnsupportedPlatform) { - return KVStoreService.encryptionKey; + return KVStoreService().encryptionKey; } try { final value = await _storage.read(key: 'encryption'); @@ -38,20 +38,20 @@ abstract class EncryptedKvStoreService { return value; } catch (e) { - return KVStoreService.encryptionKey; + return KVStoreService().encryptionKey; } } static Future setEncryptionKey(String key) async { if (isUnsupportedPlatform) { - await KVStoreService.setEncryptionKey(key); + await KVStoreService().setEncryptionKey(key); return; } try { await _storage.write(key: 'encryption', value: key); } catch (e) { - await KVStoreService.setEncryptionKey(key); + await KVStoreService().setEncryptionKey(key); } finally { _encryptionKeySync = key; } diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index e334322e..012b60d7 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -2,35 +2,37 @@ import 'dart:convert'; import 'package:encrypt/encrypt.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/collections/vars.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:uuid/uuid.dart'; -abstract class KVStoreService { - static SharedPreferences? _sharedPreferences; - static SharedPreferences get sharedPreferences => _sharedPreferences!; - - static Future initialize() async { - _sharedPreferences = await SharedPreferences.getInstance(); +final class KVStoreService { + factory KVStoreService() { + return getIt(); } - static bool get doneGettingStarted => + KVStoreService.init(); + + SharedPreferences get sharedPreferences => getIt(); + + bool get doneGettingStarted => sharedPreferences.getBool('doneGettingStarted') ?? false; - static Future setDoneGettingStarted(bool value) async => + Future setDoneGettingStarted(bool value) async => await sharedPreferences.setBool('doneGettingStarted', value); - static bool get askedForBatteryOptimization => + bool get askedForBatteryOptimization => sharedPreferences.getBool('askedForBatteryOptimization') ?? false; - static Future setAskedForBatteryOptimization(bool value) async => + Future setAskedForBatteryOptimization(bool value) async => await sharedPreferences.setBool('askedForBatteryOptimization', value); - static List get recentSearches => + List get recentSearches => sharedPreferences.getStringList('recentSearches') ?? []; - static Future setRecentSearches(List value) async => + Future setRecentSearches(List value) async => await sharedPreferences.setStringList('recentSearches', value); - static WindowSize? get windowSize { + WindowSize? get windowSize { final raw = sharedPreferences.getString('windowSize'); if (raw == null) { @@ -39,7 +41,7 @@ abstract class KVStoreService { return WindowSize.fromJson(jsonDecode(raw)); } - static Future setWindowSize(WindowSize value) async => + Future setWindowSize(WindowSize value) async => await sharedPreferences.setString( 'windowSize', jsonEncode( @@ -47,7 +49,7 @@ abstract class KVStoreService { ), ); - static String get encryptionKey { + String get encryptionKey { final value = sharedPreferences.getString('encryption'); final key = const Uuid().v4(); @@ -59,11 +61,11 @@ abstract class KVStoreService { return value; } - static Future setEncryptionKey(String key) async { + Future setEncryptionKey(String key) async { await sharedPreferences.setString('encryption', key); } - static IV get ivKey { + IV get ivKey { final iv = sharedPreferences.getString('iv'); final value = IV.fromSecureRandom(8); @@ -76,20 +78,20 @@ abstract class KVStoreService { return IV.fromBase64(iv); } - static Future setIVKey(IV iv) async { + Future setIVKey(IV iv) async { await sharedPreferences.setString('iv', iv.base64); } - static double get volume => sharedPreferences.getDouble('volume') ?? 1.0; - static Future setVolume(double value) async => + double get volume => sharedPreferences.getDouble('volume') ?? 1.0; + Future setVolume(double value) async => await sharedPreferences.setDouble('volume', value); - static bool get hasMigratedToDrift => + bool get hasMigratedToDrift => sharedPreferences.getBool('hasMigratedToDrift') ?? false; - static Future setHasMigratedToDrift(bool value) async => + Future setHasMigratedToDrift(bool value) async => await sharedPreferences.setBool('hasMigratedToDrift', value); - static Map? get _youtubeEnginePaths { + Map? get _youtubeEnginePaths { final jsonRaw = sharedPreferences.getString('ytDlpPath'); if (jsonRaw == null) { @@ -99,11 +101,11 @@ abstract class KVStoreService { return jsonDecode(jsonRaw); } - static String? getYoutubeEnginePath(YoutubeClientEngine engine) { + String? getYoutubeEnginePath(YoutubeClientEngine engine) { return _youtubeEnginePaths?[engine.name]; } - static Future setYoutubeEnginePath( + Future setYoutubeEnginePath( YoutubeClientEngine engine, String path, ) async { diff --git a/lib/services/wm_tools/wm_tools.dart b/lib/services/wm_tools/wm_tools.dart index f60b4ac9..26f9bfac 100644 --- a/lib/services/wm_tools/wm_tools.dart +++ b/lib/services/wm_tools/wm_tools.dart @@ -47,7 +47,7 @@ class WindowManagerTools with WidgetsBindingObserver { center: true, ), () async { - final savedSize = KVStoreService.windowSize; + final savedSize = KVStoreService().windowSize; await windowManager.setResizable(true); if (savedSize?.maximized == true && !(await windowManager.isMaximized())) { @@ -77,7 +77,7 @@ class WindowManagerTools with WidgetsBindingObserver { return; } final isMaximized = await windowManager.isMaximized(); - await KVStoreService.setWindowSize( + await KVStoreService().setWindowSize( WindowSize( height: size.height, width: size.width, diff --git a/pubspec.lock b/pubspec.lock index f1073dd8..6106e016 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1106,6 +1106,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + url: "https://pub.dev" + source: hosted + version: "8.0.3" glob: dependency: transitive description: @@ -1566,7 +1574,7 @@ packages: sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mocktail: dependency: "direct dev" description: @@ -1575,7 +1583,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - version: "2.0.0" nm: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7cb693f5..d129992b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -142,6 +142,7 @@ dependencies: collection: any otp_util: ^1.0.2 dio_http2_adapter: ^2.6.0 + get_it: ^8.0.3 dev_dependencies: build_runner: ^2.4.13 diff --git a/test/provider/volume_provider_test.dart b/test/provider/volume_provider_test.dart new file mode 100644 index 00000000..8484ca87 --- /dev/null +++ b/test/provider/volume_provider_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/volume_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; + +void main() { + late ProviderContainer container; + late VolumeProvider volumeProvider; + + setUp(() { + container = ProviderContainer(); + volumeProvider = container.read(volumeProvider.notifier); + }); + + tearDown(() { + container.dispose(); + }); + + test('initial volume is set from KVStore', () { + expect(container.read(volumeProvider), KVStoreService().volume); + }); + + test('setVolume updates state and KVStore', () async { + const testVolume = 0.75; + await volumeProvider.setVolume(testVolume); + + expect(container.read(volumeProvider), testVolume); + expect(KVStoreService().volume, testVolume); + }); + + test('setVolume updates audio player', () async { + const testVolume = 0.5; + await volumeProvider.setVolume(testVolume); + + // Verify that the audio player's volume was set + // Note: This assumes audioPlayer.setVolume is properly mocked or can be verified + expect(audioPlayer.volume, testVolume); + }); +}