diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 412e6868..21137174 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -54,7 +54,7 @@ part 'typeconverters/subtitle.dart'; ], ) class AppDatabase extends _$AppDatabase { - AppDatabase() : super(_openConnection()); + AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override int get schemaVersion => 1; diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index ae4ec1e8..c42cd381 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -111,7 +111,7 @@ class PreferencesTable extends Table { market: Market.US, searchMode: SearchMode.youtube, downloadLocation: "", - localLibraryLocation: [], + localLibraryLocation: const [], pipedInstance: "https://pipedapi.kavin.rocks", themeMode: ThemeMode.system, audioSource: AudioSource.youtube, diff --git a/lib/modules/settings/color_scheme_picker_dialog.dart b/lib/modules/settings/color_scheme_picker_dialog.dart index f2933505..47b4b9a0 100644 --- a/lib/modules/settings/color_scheme_picker_dialog.dart +++ b/lib/modules/settings/color_scheme_picker_dialog.dart @@ -22,6 +22,14 @@ class SpotubeColor extends Color { String toString() { return "$name:$value"; } + + @override + operator ==(Object other) { + return other is SpotubeColor && other.value == value && other.name == name; + } + + @override + int get hashCode => Object.hashAll([value, name]); } final Set colorsMap = { diff --git a/lib/provider/tray_manager/tray_manager.dart b/lib/provider/tray_manager/tray_manager.dart index 9cc4becc..d4da60c4 100644 --- a/lib/provider/tray_manager/tray_manager.dart +++ b/lib/provider/tray_manager/tray_manager.dart @@ -1,9 +1,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/tray_manager/tray_menu.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/window_manager/window_manager.dart'; import 'package:spotube/utils/platform.dart'; import 'package:tray_manager/tray_manager.dart'; -import 'package:window_manager/window_manager.dart'; class SystemTrayManager with TrayListener { final Ref ref; @@ -40,7 +40,7 @@ class SystemTrayManager with TrayListener { @override onTrayIconMouseDown() { if (kIsWindows) { - windowManager.show(); + ref.read(windowManagerProvider).show(); } else { trayManager.popUpContextMenu(); } @@ -49,7 +49,7 @@ class SystemTrayManager with TrayListener { @override onTrayIconRightMouseDown() { if (!kIsWindows) { - windowManager.show(); + ref.read(windowManagerProvider).show(); } else { trayManager.popUpContextMenu(); } diff --git a/lib/provider/tray_manager/tray_menu.dart b/lib/provider/tray_manager/tray_menu.dart index 42a3f948..3944b08f 100644 --- a/lib/provider/tray_manager/tray_menu.dart +++ b/lib/provider/tray_manager/tray_menu.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/window_manager/window_manager.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:tray_manager/tray_manager.dart'; -import 'package:window_manager/window_manager.dart'; final audioPlayerLoopMode = StreamProvider((ref) { return audioPlayer.loopModeStream; @@ -19,6 +19,8 @@ final audioPlayerPlaying = StreamProvider((ref) { }); final trayMenuProvider = Provider((ref) { + final windowManager = ref.watch(windowManagerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isPlaybackPlaying = ref.watch(audioPlayerProvider.select((s) => s.activeTrack != null)); diff --git a/lib/provider/user_preferences/default_download_dir_provider.dart b/lib/provider/user_preferences/default_download_dir_provider.dart new file mode 100644 index 00000000..8ed00573 --- /dev/null +++ b/lib/provider/user_preferences/default_download_dir_provider.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotube/utils/platform.dart'; + +final defaultDownloadDirectoryProvider = FutureProvider((ref) async { + if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; + + if (kIsMacOS) { + return join((await getLibraryDirectory()).path, "Caches"); + } + + return getDownloadsDirectory().then((dir) { + return join(dir!.path, "Spotube"); + }); +}); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 23479b71..cedfe78b 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -1,14 +1,14 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; 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/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/palette_provider.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'; @@ -29,7 +29,9 @@ class UserPreferencesNotifier extends Notifier { await db.into(db.preferencesTable).insert( PreferencesTableCompanion.insert( id: const Value(0), - downloadLocation: Value(await _getDefaultDownloadDirectory()), + downloadLocation: Value( + await ref.read(defaultDownloadDirectoryProvider.future), + ), ), ); } @@ -46,11 +48,11 @@ class UserPreferencesNotifier extends Notifier { state = event; if (kIsDesktop) { - await windowManager.setTitleBarStyle( - state.systemTitleBar - ? TitleBarStyle.normal - : TitleBarStyle.hidden, - ); + await ref.read(windowManagerProvider).setTitleBarStyle( + state.systemTitleBar + ? TitleBarStyle.normal + : TitleBarStyle.hidden, + ); } await audioPlayer.setAudioNormalization(state.normalizeAudio); @@ -67,18 +69,6 @@ class UserPreferencesNotifier extends Notifier { return PreferencesTable.defaults(); } - Future _getDefaultDownloadDirectory() async { - if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; - - if (kIsMacOS) { - return join((await getLibraryDirectory()).path, "Caches"); - } - - return getDownloadsDirectory().then((dir) { - return join(dir!.path, "Spotube"); - }); - } - Future setData(PreferencesTableCompanion data) async { final db = ref.read(databaseProvider); @@ -95,28 +85,28 @@ class UserPreferencesNotifier extends Notifier { await query.replace(PreferencesTableCompanion.insert()); } - void setStreamMusicCodec(SourceCodecs codec) { - setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); + Future setStreamMusicCodec(SourceCodecs codec) async { + await setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); } - void setDownloadMusicCodec(SourceCodecs codec) { - setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec))); + Future setDownloadMusicCodec(SourceCodecs codec) async { + await setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec))); } - void setThemeMode(ThemeMode mode) { - setData(PreferencesTableCompanion(themeMode: Value(mode))); + Future setThemeMode(ThemeMode mode) async { + await setData(PreferencesTableCompanion(themeMode: Value(mode))); } - void setRecommendationMarket(Market country) { - setData(PreferencesTableCompanion(market: Value(country))); + Future setRecommendationMarket(Market country) async { + await setData(PreferencesTableCompanion(market: Value(country))); } - void setAccentColorScheme(SpotubeColor color) { - setData(PreferencesTableCompanion(accentColorScheme: Value(color))); + Future setAccentColorScheme(SpotubeColor color) async { + await setData(PreferencesTableCompanion(accentColorScheme: Value(color))); } - void setAlbumColorSync(bool sync) { - setData(PreferencesTableCompanion(albumColorSync: Value(sync))); + Future setAlbumColorSync(bool sync) async { + await setData(PreferencesTableCompanion(albumColorSync: Value(sync))); if (!sync) { ref.read(paletteProvider.notifier).state = null; @@ -125,87 +115,88 @@ class UserPreferencesNotifier extends Notifier { } } - void setCheckUpdate(bool check) { - setData(PreferencesTableCompanion(checkUpdate: Value(check))); + Future setCheckUpdate(bool check) async { + await setData(PreferencesTableCompanion(checkUpdate: Value(check))); } - void setAudioQuality(SourceQualities quality) { - setData(PreferencesTableCompanion(audioQuality: Value(quality))); + Future setAudioQuality(SourceQualities quality) async { + await setData(PreferencesTableCompanion(audioQuality: Value(quality))); } - void setDownloadLocation(String downloadDir) { + Future setDownloadLocation(String downloadDir) async { if (downloadDir.isEmpty) return; - setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir))); + await setData( + PreferencesTableCompanion(downloadLocation: Value(downloadDir))); } - void setLocalLibraryLocation(List localLibraryDirs) { - //if (localLibraryDir.isEmpty) return; - setData( + Future setLocalLibraryLocation(List localLibraryDirs) async { + await setData( PreferencesTableCompanion( localLibraryLocation: Value(localLibraryDirs), ), ); } - void setLayoutMode(LayoutMode mode) { - setData(PreferencesTableCompanion(layoutMode: Value(mode))); + Future setLayoutMode(LayoutMode mode) async { + await setData(PreferencesTableCompanion(layoutMode: Value(mode))); } - void setCloseBehavior(CloseBehavior behavior) { - setData(PreferencesTableCompanion(closeBehavior: Value(behavior))); + Future setCloseBehavior(CloseBehavior behavior) async { + await setData(PreferencesTableCompanion(closeBehavior: Value(behavior))); } - void setShowSystemTrayIcon(bool show) { - setData(PreferencesTableCompanion(showSystemTrayIcon: Value(show))); + Future setShowSystemTrayIcon(bool show) async { + await setData(PreferencesTableCompanion(showSystemTrayIcon: Value(show))); } - void setLocale(Locale locale) { - setData(PreferencesTableCompanion(locale: Value(locale))); + Future setLocale(Locale locale) async { + await setData(PreferencesTableCompanion(locale: Value(locale))); } - void setPipedInstance(String instance) { - setData(PreferencesTableCompanion(pipedInstance: Value(instance))); + Future setPipedInstance(String instance) async { + await setData(PreferencesTableCompanion(pipedInstance: Value(instance))); } - void setSearchMode(SearchMode mode) { - setData(PreferencesTableCompanion(searchMode: Value(mode))); + Future setSearchMode(SearchMode mode) async { + await setData(PreferencesTableCompanion(searchMode: Value(mode))); } - void setSkipNonMusic(bool skip) { - setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); + Future setSkipNonMusic(bool skip) async { + await setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); } - void setAudioSource(AudioSource type) { - setData(PreferencesTableCompanion(audioSource: Value(type))); + Future setAudioSource(AudioSource type) async { + await setData(PreferencesTableCompanion(audioSource: Value(type))); } - void setSystemTitleBar(bool isSystemTitleBar) { - setData( + Future setSystemTitleBar(bool isSystemTitleBar) async { + await setData( PreferencesTableCompanion( systemTitleBar: Value(isSystemTitleBar), ), ); } - void setDiscordPresence(bool discordPresence) { - setData(PreferencesTableCompanion(discordPresence: Value(discordPresence))); + Future setDiscordPresence(bool discordPresence) async { + await setData( + PreferencesTableCompanion(discordPresence: Value(discordPresence))); } - void setAmoledDarkTheme(bool isAmoled) { - setData(PreferencesTableCompanion(amoledDarkTheme: Value(isAmoled))); + Future setAmoledDarkTheme(bool isAmoled) async { + await setData(PreferencesTableCompanion(amoledDarkTheme: Value(isAmoled))); } - void setNormalizeAudio(bool normalize) { - setData(PreferencesTableCompanion(normalizeAudio: Value(normalize))); + Future setNormalizeAudio(bool normalize) async { + await setData(PreferencesTableCompanion(normalizeAudio: Value(normalize))); audioPlayer.setAudioNormalization(normalize); } - void setEndlessPlayback(bool endless) { - setData(PreferencesTableCompanion(endlessPlayback: Value(endless))); + Future setEndlessPlayback(bool endless) async { + await setData(PreferencesTableCompanion(endlessPlayback: Value(endless))); } - void setEnableConnect(bool enable) { - setData(PreferencesTableCompanion(enableConnect: Value(enable))); + Future setEnableConnect(bool enable) async { + await setData(PreferencesTableCompanion(enableConnect: Value(enable))); } } diff --git a/lib/provider/window_manager/window_manager.dart b/lib/provider/window_manager/window_manager.dart new file mode 100644 index 00000000..3bb5ad35 --- /dev/null +++ b/lib/provider/window_manager/window_manager.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:window_manager/window_manager.dart'; + +final windowManagerProvider = Provider((ref) => windowManager); diff --git a/pubspec.lock b/pubspec.lock index 089563d8..4b3b5e6d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -369,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" cross_file: dependency: transitive description: @@ -1432,6 +1440,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" oauth2: dependency: transitive description: @@ -1849,6 +1873,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" shelf_router: dependency: "direct main" description: @@ -1857,6 +1889,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: "direct main" description: @@ -1935,6 +1975,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -2087,6 +2143,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: @@ -2095,6 +2159,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" time: dependency: transitive description: @@ -2327,6 +2399,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" wikipedia_api: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e4face3c..1d638fd0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -158,6 +158,8 @@ dev_dependencies: xml: ^6.5.0 io: ^1.0.4 drift_dev: ^2.18.0 + test: ^1.25.2 + mocktail: ^1.0.4 dependency_overrides: uuid: ^4.4.0 diff --git a/test/providers/create_container.dart b/test/providers/create_container.dart new file mode 100644 index 00000000..6d5a4210 --- /dev/null +++ b/test/providers/create_container.dart @@ -0,0 +1,22 @@ +import 'package:riverpod/riverpod.dart'; +import 'package:test/test.dart'; + +/// A testing utility which creates a [ProviderContainer] and automatically +/// disposes it at the end of the test. +ProviderContainer createContainer({ + ProviderContainer? parent, + List overrides = const [], + List? observers, +}) { + // Create a ProviderContainer, and optionally allow specifying parameters. + final container = ProviderContainer( + parent: parent, + overrides: overrides, + observers: observers, + ); + + // When the test ends, dispose the container. + addTearDown(container.dispose); + + return container; +} \ No newline at end of file diff --git a/test/providers/mocks/audio_player_listeners_mock.dart b/test/providers/mocks/audio_player_listeners_mock.dart new file mode 100644 index 00000000..300e411f --- /dev/null +++ b/test/providers/mocks/audio_player_listeners_mock.dart @@ -0,0 +1,5 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:spotube/provider/audio_player/audio_player_streams.dart'; + +class MockAudioPlayerStreamListeners extends Mock + implements AudioPlayerStreamListeners {} diff --git a/test/providers/mocks/window_manager_mock.dart b/test/providers/mocks/window_manager_mock.dart new file mode 100644 index 00000000..a1ac0e34 --- /dev/null +++ b/test/providers/mocks/window_manager_mock.dart @@ -0,0 +1,4 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:window_manager/window_manager.dart'; + +class MockWindowManager extends Mock implements WindowManager {} diff --git a/test/providers/user_preferences/user_preferences_test.dart b/test/providers/user_preferences/user_preferences_test.dart new file mode 100644 index 00000000..3e884fa8 --- /dev/null +++ b/test/providers/user_preferences/user_preferences_test.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/audio_player/audio_player_streams.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/user_preferences/default_download_dir_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/window_manager/window_manager.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../create_container.dart'; +import '../mocks/audio_player_listeners_mock.dart'; +import '../mocks/window_manager_mock.dart'; + +List _createDefaultOverrides() => [ + databaseProvider.overrideWith( + (ref) { + final database = AppDatabase(NativeDatabase.memory()); + + ref.onDispose(database.close); + return database; + }, + ), + audioPlayerStreamListenersProvider.overrideWith( + (ref) { + final streamListeners = MockAudioPlayerStreamListeners(); + + when(() => streamListeners.updatePalette()).thenReturn( + Future.value(), + ); + + return streamListeners; + }, + ), + defaultDownloadDirectoryProvider.overrideWith( + (ref) { + return Future.value("/storage/emulated/0/Download/Spotube"); + }, + ) + ]; + +void main() { + group('UserPreferences', () { + setUpAll(() { + registerFallbackValue(TitleBarStyle.normal); + AppLogger.initialize(false); + }); + + test('Initial value should be equal the default values', () { + final ref = createContainer(overrides: _createDefaultOverrides()); + + final preferences = ref.read(userPreferencesProvider); + final defaultPreferences = PreferencesTable.defaults(); + + expect(preferences, defaultPreferences); + }); + + test('[setSystemTitleBar] should update UI titlebar', () async { + TestWidgetsFlutterBinding.ensureInitialized(); + + final ref = createContainer(overrides: [ + ..._createDefaultOverrides(), + windowManagerProvider.overrideWith( + (ref) { + final mockWindowManager = MockWindowManager(); + + when(() => mockWindowManager.setTitleBarStyle(any())) + .thenAnswer((_) => Future.value()); + + return mockWindowManager; + }, + ) + ]); + + final db = ref.read(databaseProvider); + final preferences = ref.read(userPreferencesProvider); + await Future.delayed(const Duration(milliseconds: 300)); + final preferencesNotifier = ref.read(userPreferencesProvider.notifier); + + expect(preferences.systemTitleBar, false); + + await preferencesNotifier.setSystemTitleBar(true); + + final completer = Completer(); + final subscription = (db.select(db.preferencesTable) + ..where((tbl) => tbl.id.equals(0))) + .watchSingle() + .listen((event) { + completer.complete(event.systemTitleBar); + }); + + addTearDown(() { + subscription.cancel(); + }); + + final systemTitleBar = await completer.future; + + expect(systemTitleBar, true); + verify( + () => ref + .read(windowManagerProvider) + .setTitleBarStyle(TitleBarStyle.normal), + ).called(1); + }); + }); +}