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",
"*.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 {
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);

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 =
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp);
KVStoreService().getYoutubeEnginePath(YoutubeClientEngine.ytDlp);
if (youtubeEngine == YoutubeClientEngine.ytDlp &&
!await YtDlpEngine.isInstalled() &&

View File

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

View File

@ -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<void> main(List<String> rawArgs) async {
AppLogger.runZoned(() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
GetIt.I.registerSingleton<SharedPreferences>(
await SharedPreferences.getInstance(),
);
GetIt.I.registerSingletonWithDependencies<KVStoreService>(
() => KVStoreService.init(),
dependsOn: [SharedPreferences],
);
await registerWindowsScheme("spotify");
tz.initializeTimeZones();
@ -85,13 +95,11 @@ Future<void> main(List<String> 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);

View File

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

View File

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

View File

@ -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(

View File

@ -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(

View File

@ -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()) &&

View File

@ -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';

View File

@ -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<double> {
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<void> setVolume(double volume) async {
state = volume;
await audioPlayer.setVolume(volume);
KVStoreService.setVolume(volume);
await _audioPlayer.setVolume(volume);
KVStoreService().setVolume(volume);
}
}
final volumeProvider =
NotifierProvider<VolumeProvider, double>(() => VolumeProvider());
final volumeProvider = NotifierProvider<VolumeProvider, double>(() {
return VolumeProvider(audioPlayer: audioPlayer);
});

View File

@ -25,7 +25,7 @@ abstract class EncryptedKvStoreService {
static Future<String> 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<void> 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;
}

View File

@ -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<void> initialize() async {
_sharedPreferences = await SharedPreferences.getInstance();
final class KVStoreService {
factory KVStoreService() {
return getIt<KVStoreService>();
}
static bool get doneGettingStarted =>
KVStoreService.init();
SharedPreferences get sharedPreferences => getIt<SharedPreferences>();
bool get doneGettingStarted =>
sharedPreferences.getBool('doneGettingStarted') ?? false;
static Future<void> setDoneGettingStarted(bool value) async =>
Future<void> setDoneGettingStarted(bool value) async =>
await sharedPreferences.setBool('doneGettingStarted', value);
static bool get askedForBatteryOptimization =>
bool get askedForBatteryOptimization =>
sharedPreferences.getBool('askedForBatteryOptimization') ?? false;
static Future<void> setAskedForBatteryOptimization(bool value) async =>
Future<void> setAskedForBatteryOptimization(bool value) async =>
await sharedPreferences.setBool('askedForBatteryOptimization', value);
static List<String> get recentSearches =>
List<String> get recentSearches =>
sharedPreferences.getStringList('recentSearches') ?? [];
static Future<void> setRecentSearches(List<String> value) async =>
Future<void> setRecentSearches(List<String> 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<void> setWindowSize(WindowSize value) async =>
Future<void> 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<void> setEncryptionKey(String key) async {
Future<void> 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<void> setIVKey(IV iv) async {
Future<void> setIVKey(IV iv) async {
await sharedPreferences.setString('iv', iv.base64);
}
static double get volume => sharedPreferences.getDouble('volume') ?? 1.0;
static Future<void> setVolume(double value) async =>
double get volume => sharedPreferences.getDouble('volume') ?? 1.0;
Future<void> setVolume(double value) async =>
await sharedPreferences.setDouble('volume', value);
static bool get hasMigratedToDrift =>
bool get hasMigratedToDrift =>
sharedPreferences.getBool('hasMigratedToDrift') ?? false;
static Future<void> setHasMigratedToDrift(bool value) async =>
Future<void> setHasMigratedToDrift(bool value) async =>
await sharedPreferences.setBool('hasMigratedToDrift', value);
static Map<String, dynamic>? get _youtubeEnginePaths {
Map<String, dynamic>? 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<void> setYoutubeEnginePath(
Future<void> setYoutubeEnginePath(
YoutubeClientEngine engine,
String path,
) async {

View File

@ -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,

View File

@ -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:

View File

@ -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

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