test: add get_it and turn ever KVStore into a singleton service

This commit is contained in:
Kingkor Roy Tirtho 2025-04-14 22:34:31 +06:00
parent 3b25b227d6
commit 472f379f3d
19 changed files with 129 additions and 58 deletions

View File

@ -29,5 +29,6 @@
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md", "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": "${capture}.g.dart,${capture}.freezed.dart"
}, },
"dart.flutterSdkPath": ".fvm/versions/3.29.2" "dart.flutterSdkPath": ".fvm/versions/3.29.2",
"makefile.configureOnOpen": false
} }

View File

@ -30,7 +30,7 @@ class AppRouter extends RootStackRouter {
(resolver, router) async { (resolver, router) async {
final auth = await ref.read(authenticationProvider.future); final auth = await ref.read(authenticationProvider.future);
if (auth == null && !KVStoreService.doneGettingStarted) { if (auth == null && !KVStoreService().doneGettingStarted) {
resolver.redirect(const GettingStartedRoute()); resolver.redirect(const GettingStartedRoute());
} else { } else {
resolver.next(true); resolver.next(true);

View File

@ -0,0 +1,3 @@
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;

View File

@ -21,7 +21,7 @@ void useCheckYtDlpInstalled(WidgetRef ref) {
); );
final customPath = final customPath =
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp); KVStoreService().getYoutubeEnginePath(YoutubeClientEngine.ytDlp);
if (youtubeEngine == YoutubeClientEngine.ytDlp && if (youtubeEngine == YoutubeClientEngine.ytDlp &&
!await YtDlpEngine.isInstalled() && !await YtDlpEngine.isInstalled() &&

View File

@ -6,7 +6,7 @@ import 'package:spotube/utils/platform.dart';
void useDisableBatteryOptimizations() { void useDisableBatteryOptimizations() {
useAsyncEffect(() async { useAsyncEffect(() async {
if (!kIsAndroid || KVStoreService.askedForBatteryOptimization) return; if (!kIsAndroid || KVStoreService().askedForBatteryOptimization) return;
await DisableBatteryOptimization.showDisableBatteryOptimizationSettings(); await DisableBatteryOptimization.showDisableBatteryOptimizationSettings();
@ -16,6 +16,6 @@ void useDisableBatteryOptimizations() {
"Follow the steps and disable the optimizations to allow smooth functioning of this app", "Follow the steps and disable the optimizations to allow smooth functioning of this app",
); );
await KVStoreService.setAskedForBatteryOptimization(true); await KVStoreService().setAskedForBatteryOptimization(true);
}, null, []); }, null, []);
} }

View File

@ -8,12 +8,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get_it/get_it.dart';
import 'package:home_widget/home_widget.dart'; import 'package:home_widget/home_widget.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:smtc_windows/smtc_windows.dart'; import 'package:smtc_windows/smtc_windows.dart';
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/initializers.dart';
@ -65,6 +67,14 @@ Future<void> main(List<String> rawArgs) async {
AppLogger.runZoned(() async { AppLogger.runZoned(() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
GetIt.I.registerSingleton<SharedPreferences>(
await SharedPreferences.getInstance(),
);
GetIt.I.registerSingletonWithDependencies<KVStoreService>(
() => KVStoreService.init(),
dependsOn: [SharedPreferences],
);
await registerWindowsScheme("spotify"); await registerWindowsScheme("spotify");
tz.initializeTimeZones(); tz.initializeTimeZones();
@ -85,13 +95,11 @@ Future<void> main(List<String> rawArgs) async {
MetadataGod.initialize(); MetadataGod.initialize();
} }
await KVStoreService.initialize();
if (kIsDesktop) { if (kIsDesktop) {
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);
await YtDlp.instance await YtDlp.instance
.setBinaryLocation( .setBinaryLocation(
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ?? KVStoreService().getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ??
"yt-dlp${kIsWindows ? '.exe' : ''}", "yt-dlp${kIsWindows ? '.exe' : ''}",
) )
.catchError((e, stack) => null); .catchError((e, stack) => null);

View File

@ -16,7 +16,7 @@ class DecryptedText {
return DecryptedText( return DecryptedText(
_encrypter!.decrypt( _encrypter!.decrypt(
Encrypted.fromBase64(value), Encrypted.fromBase64(value),
iv: KVStoreService.ivKey, iv: KVStoreService().ivKey,
), ),
); );
} }
@ -27,7 +27,7 @@ class DecryptedText {
Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync), Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync),
), ),
); );
return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64; return _encrypter!.encrypt(value, iv: KVStoreService().ivKey).base64;
} }
} }

View File

@ -103,7 +103,7 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
?.invalidate(context.l10n.file_not_found); ?.invalidate(context.l10n.file_not_found);
return; return;
} }
await KVStoreService.setYoutubeEnginePath( await KVStoreService().setYoutubeEnginePath(
engine, engine,
controller.text, controller.text,
); );

View File

@ -110,7 +110,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
Button.secondary( Button.secondary(
leading: const Icon(SpotubeIcons.anonymous), leading: const Icon(SpotubeIcons.anonymous),
onPressed: () async { onPressed: () async {
await KVStoreService.setDoneGettingStarted(true); await KVStoreService().setDoneGettingStarted(true);
if (context.mounted) { if (context.mounted) {
context.navigateTo(const HomeRoute()); context.navigateTo(const HomeRoute());
} }
@ -134,7 +134,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
}, },
), ),
onPressed: () async { onPressed: () async {
await KVStoreService.setDoneGettingStarted(true); await KVStoreService().setDoneGettingStarted(true);
await onLogin(); await onLogin();
}, },
child: Text( child: Text(

View File

@ -62,10 +62,10 @@ class SearchPage extends HookConsumerWidget {
if (value.trim().isEmpty) { if (value.trim().isEmpty) {
return; return;
} }
KVStoreService.setRecentSearches( KVStoreService().setRecentSearches(
{ {
value, value,
...KVStoreService.recentSearches, ...KVStoreService().recentSearches,
}.toList(), }.toList(),
); );
} }
@ -96,8 +96,9 @@ class SearchPage extends HookConsumerWidget {
listenable: controller, listenable: controller,
builder: (context, _) { builder: (context, _) {
final suggestions = controller.text.isEmpty final suggestions = controller.text.isEmpty
? KVStoreService.recentSearches ? KVStoreService().recentSearches
: KVStoreService.recentSearches : KVStoreService()
.recentSearches
.where( .where(
(s) => (s) =>
weightedRatio( weightedRatio(

View File

@ -410,7 +410,8 @@ class SettingsPlaybackSection extends HookConsumerWidget {
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
if (value == YoutubeClientEngine.ytDlp) { if (value == YoutubeClientEngine.ytDlp) {
final customPath = KVStoreService.getYoutubeEnginePath(value); final customPath =
KVStoreService().getYoutubeEnginePath(value);
if (!await YtDlpEngine.isInstalled() && if (!await YtDlpEngine.isInstalled() &&
(customPath == null || (customPath == null ||
!await File(customPath).exists()) && !await File(customPath).exists()) &&

View File

@ -8,6 +8,8 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:spotube/provider/database/database.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/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';

View File

@ -5,20 +5,25 @@ import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
class VolumeProvider extends Notifier<double> { class VolumeProvider extends Notifier<double> {
VolumeProvider(); final SpotubeAudioPlayer _audioPlayer;
VolumeProvider({
required SpotubeAudioPlayer audioPlayer,
}) : _audioPlayer = audioPlayer;
@override @override
build() { build() {
audioPlayer.setVolume(KVStoreService.volume); _audioPlayer.setVolume(KVStoreService().volume);
return KVStoreService.volume; return KVStoreService().volume;
} }
Future<void> setVolume(double volume) async { Future<void> setVolume(double volume) async {
state = volume; state = volume;
await audioPlayer.setVolume(volume); await _audioPlayer.setVolume(volume);
KVStoreService.setVolume(volume); KVStoreService().setVolume(volume);
} }
} }
final volumeProvider = final volumeProvider = NotifierProvider<VolumeProvider, double>(() {
NotifierProvider<VolumeProvider, double>(() => VolumeProvider()); return VolumeProvider(audioPlayer: audioPlayer);
});

View File

@ -25,7 +25,7 @@ abstract class EncryptedKvStoreService {
static Future<String> get encryptionKey async { static Future<String> get encryptionKey async {
if (isUnsupportedPlatform) { if (isUnsupportedPlatform) {
return KVStoreService.encryptionKey; return KVStoreService().encryptionKey;
} }
try { try {
final value = await _storage.read(key: 'encryption'); final value = await _storage.read(key: 'encryption');
@ -38,20 +38,20 @@ abstract class EncryptedKvStoreService {
return value; return value;
} catch (e) { } catch (e) {
return KVStoreService.encryptionKey; return KVStoreService().encryptionKey;
} }
} }
static Future<void> setEncryptionKey(String key) async { static Future<void> setEncryptionKey(String key) async {
if (isUnsupportedPlatform) { if (isUnsupportedPlatform) {
await KVStoreService.setEncryptionKey(key); await KVStoreService().setEncryptionKey(key);
return; return;
} }
try { try {
await _storage.write(key: 'encryption', value: key); await _storage.write(key: 'encryption', value: key);
} catch (e) { } catch (e) {
await KVStoreService.setEncryptionKey(key); await KVStoreService().setEncryptionKey(key);
} finally { } finally {
_encryptionKeySync = key; _encryptionKeySync = key;
} }

View File

@ -2,35 +2,37 @@ import 'dart:convert';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/vars.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
abstract class KVStoreService { final class KVStoreService {
static SharedPreferences? _sharedPreferences; factory KVStoreService() {
static SharedPreferences get sharedPreferences => _sharedPreferences!; return getIt<KVStoreService>();
static Future<void> initialize() async {
_sharedPreferences = await SharedPreferences.getInstance();
} }
static bool get doneGettingStarted => KVStoreService.init();
SharedPreferences get sharedPreferences => getIt<SharedPreferences>();
bool get doneGettingStarted =>
sharedPreferences.getBool('doneGettingStarted') ?? false; sharedPreferences.getBool('doneGettingStarted') ?? false;
static Future<void> setDoneGettingStarted(bool value) async => Future<void> setDoneGettingStarted(bool value) async =>
await sharedPreferences.setBool('doneGettingStarted', value); await sharedPreferences.setBool('doneGettingStarted', value);
static bool get askedForBatteryOptimization => bool get askedForBatteryOptimization =>
sharedPreferences.getBool('askedForBatteryOptimization') ?? false; sharedPreferences.getBool('askedForBatteryOptimization') ?? false;
static Future<void> setAskedForBatteryOptimization(bool value) async => Future<void> setAskedForBatteryOptimization(bool value) async =>
await sharedPreferences.setBool('askedForBatteryOptimization', value); await sharedPreferences.setBool('askedForBatteryOptimization', value);
static List<String> get recentSearches => List<String> get recentSearches =>
sharedPreferences.getStringList('recentSearches') ?? []; sharedPreferences.getStringList('recentSearches') ?? [];
static Future<void> setRecentSearches(List<String> value) async => Future<void> setRecentSearches(List<String> value) async =>
await sharedPreferences.setStringList('recentSearches', value); await sharedPreferences.setStringList('recentSearches', value);
static WindowSize? get windowSize { WindowSize? get windowSize {
final raw = sharedPreferences.getString('windowSize'); final raw = sharedPreferences.getString('windowSize');
if (raw == null) { if (raw == null) {
@ -39,7 +41,7 @@ abstract class KVStoreService {
return WindowSize.fromJson(jsonDecode(raw)); return WindowSize.fromJson(jsonDecode(raw));
} }
static Future<void> setWindowSize(WindowSize value) async => Future<void> setWindowSize(WindowSize value) async =>
await sharedPreferences.setString( await sharedPreferences.setString(
'windowSize', 'windowSize',
jsonEncode( jsonEncode(
@ -47,7 +49,7 @@ abstract class KVStoreService {
), ),
); );
static String get encryptionKey { String get encryptionKey {
final value = sharedPreferences.getString('encryption'); final value = sharedPreferences.getString('encryption');
final key = const Uuid().v4(); final key = const Uuid().v4();
@ -59,11 +61,11 @@ abstract class KVStoreService {
return value; return value;
} }
static Future<void> setEncryptionKey(String key) async { Future<void> setEncryptionKey(String key) async {
await sharedPreferences.setString('encryption', key); await sharedPreferences.setString('encryption', key);
} }
static IV get ivKey { IV get ivKey {
final iv = sharedPreferences.getString('iv'); final iv = sharedPreferences.getString('iv');
final value = IV.fromSecureRandom(8); final value = IV.fromSecureRandom(8);
@ -76,20 +78,20 @@ abstract class KVStoreService {
return IV.fromBase64(iv); return IV.fromBase64(iv);
} }
static Future<void> setIVKey(IV iv) async { Future<void> setIVKey(IV iv) async {
await sharedPreferences.setString('iv', iv.base64); await sharedPreferences.setString('iv', iv.base64);
} }
static double get volume => sharedPreferences.getDouble('volume') ?? 1.0; double get volume => sharedPreferences.getDouble('volume') ?? 1.0;
static Future<void> setVolume(double value) async => Future<void> setVolume(double value) async =>
await sharedPreferences.setDouble('volume', value); await sharedPreferences.setDouble('volume', value);
static bool get hasMigratedToDrift => bool get hasMigratedToDrift =>
sharedPreferences.getBool('hasMigratedToDrift') ?? false; sharedPreferences.getBool('hasMigratedToDrift') ?? false;
static Future<void> setHasMigratedToDrift(bool value) async => Future<void> setHasMigratedToDrift(bool value) async =>
await sharedPreferences.setBool('hasMigratedToDrift', value); await sharedPreferences.setBool('hasMigratedToDrift', value);
static Map<String, dynamic>? get _youtubeEnginePaths { Map<String, dynamic>? get _youtubeEnginePaths {
final jsonRaw = sharedPreferences.getString('ytDlpPath'); final jsonRaw = sharedPreferences.getString('ytDlpPath');
if (jsonRaw == null) { if (jsonRaw == null) {
@ -99,11 +101,11 @@ abstract class KVStoreService {
return jsonDecode(jsonRaw); return jsonDecode(jsonRaw);
} }
static String? getYoutubeEnginePath(YoutubeClientEngine engine) { String? getYoutubeEnginePath(YoutubeClientEngine engine) {
return _youtubeEnginePaths?[engine.name]; return _youtubeEnginePaths?[engine.name];
} }
static Future<void> setYoutubeEnginePath( Future<void> setYoutubeEnginePath(
YoutubeClientEngine engine, YoutubeClientEngine engine,
String path, String path,
) async { ) async {

View File

@ -47,7 +47,7 @@ class WindowManagerTools with WidgetsBindingObserver {
center: true, center: true,
), ),
() async { () async {
final savedSize = KVStoreService.windowSize; final savedSize = KVStoreService().windowSize;
await windowManager.setResizable(true); await windowManager.setResizable(true);
if (savedSize?.maximized == true && if (savedSize?.maximized == true &&
!(await windowManager.isMaximized())) { !(await windowManager.isMaximized())) {
@ -77,7 +77,7 @@ class WindowManagerTools with WidgetsBindingObserver {
return; return;
} }
final isMaximized = await windowManager.isMaximized(); final isMaximized = await windowManager.isMaximized();
await KVStoreService.setWindowSize( await KVStoreService().setWindowSize(
WindowSize( WindowSize(
height: size.height, height: size.height,
width: size.width, width: size.width,

View File

@ -1106,6 +1106,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" 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: glob:
dependency: transitive dependency: transitive
description: description:
@ -1566,7 +1574,7 @@ packages:
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "2.0.0"
mocktail: mocktail:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1575,7 +1583,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
version: "2.0.0"
nm: nm:
dependency: transitive dependency: transitive
description: description:

View File

@ -142,6 +142,7 @@ dependencies:
collection: any collection: any
otp_util: ^1.0.2 otp_util: ^1.0.2
dio_http2_adapter: ^2.6.0 dio_http2_adapter: ^2.6.0
get_it: ^8.0.3
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13

View File

@ -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);
});
}