diff --git a/lib/main.dart b/lib/main.dart index 7b9d8c32..8dbd9417 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -135,7 +135,7 @@ Future main() async { Future _bootstrapApp() async { // Initialize settings first to get saved locale final settings = await SettingsService.getInstance(); - final savedLocale = settings.getAppLocale(); + final savedLocale = settings.read(SettingsService.appLocale); // Initialize localization with saved locale LocaleSettings.setLocale(savedLocale); @@ -162,7 +162,7 @@ Future _bootstrapApp() async { // Initialize TV detection (Android leanback or Apple TV) and PiP on Android. if (Platform.isAndroid || Platform.isIOS) { - futures.add(TvDetectionService.getInstance(forceTv: settings.getForceTvMode())); + futures.add(TvDetectionService.getInstance(forceTv: settings.read(SettingsService.forceTvMode))); } if (Platform.isAndroid) { // Initialize PiP service to listen for PiP state changes (Android only). @@ -188,7 +188,7 @@ Future _bootstrapApp() async { } // Initialize logger level based on debug setting - final debugEnabled = settings.getEnableDebugLogging(); + final debugEnabled = settings.read(SettingsService.enableDebugLogging); setLoggerLevel(debugEnabled); // Log app version and git commit at startup @@ -248,7 +248,7 @@ Breadcrumb? _beforeBreadcrumb(Breadcrumb? breadcrumb, Hint _) { FutureOr _beforeSend(SentryEvent event, Hint _) { // Drop event if user opted out of crash reporting final instance = SettingsService.instanceOrNull; - if (instance != null && !instance.getCrashReporting()) return null; + if (instance != null && !instance.read(SettingsService.crashReporting)) return null; // Drop unactionable errors var exceptions = event.exceptions; @@ -530,7 +530,7 @@ class _MainAppState extends State with WidgetsBindingObserver { await downloadProvider.refreshMetadataFromCache(); final activeKey = VideoPlayerScreenState.activeRatingKey; final settings = SettingsService.instanceOrNull; - if (settings != null && settings.getAutoRemoveWatchedDownloads()) { + if (settings != null && settings.read(SettingsService.autoRemoveWatchedDownloads)) { final deleted = await downloadProvider.autoDeleteWatchedDownloads(activeRatingKey: activeKey); if (deleted.isNotEmpty) { final msg = deleted.length == 1 diff --git a/lib/providers/offline_watch_provider.dart b/lib/providers/offline_watch_provider.dart index 4ee3e8a1..26e0b51d 100644 --- a/lib/providers/offline_watch_provider.dart +++ b/lib/providers/offline_watch_provider.dart @@ -185,7 +185,7 @@ class OfflineWatchProvider extends ChangeNotifier { /// Auto-delete a download if the auto-remove setting is enabled. void _autoDeleteIfWatched(String serverId, String ratingKey) { final settings = SettingsService.instanceOrNull; - if (settings == null || !settings.getAutoRemoveWatchedDownloads()) return; + if (settings == null || !settings.read(SettingsService.autoRemoveWatchedDownloads)) return; final globalKey = buildGlobalKey(serverId, ratingKey); final meta = _downloadProvider.getMetadata(globalKey); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index d20d2cda..e055d33e 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,229 +1,81 @@ import 'package:flutter/material.dart'; import '../models/transcode_quality_preset.dart'; +import '../services/base_shared_preferences_service.dart'; import '../services/settings_service.dart'; class SettingsProvider extends ChangeNotifier { SettingsService? _settingsService; - int _libraryDensity = LibraryDensity.defaultValue; - ViewMode _viewMode = ViewMode.grid; - EpisodePosterMode _episodePosterMode = EpisodePosterMode.seriesPoster; - bool _showHeroSection = true; - bool _useGlobalHubs = true; - bool _showServerNameOnHubs = false; - bool _groupLibrariesByServer = true; - bool _alwaysKeepSidebarOpen = false; - bool _showUnwatchedCount = true; - bool _showEpisodeNumberOnCards = true; - bool _hideSpoilers = false; - bool _showNavBarLabels = true; - bool _liveTvDefaultFavorites = false; - bool _autoHidePerformanceOverlay = true; - TranscodeQualityPreset _defaultQualityPreset = TranscodeQualityPreset.original; bool _isInitialized = false; Future? _initFuture; SettingsProvider() { - // Start initialization eagerly to reduce race conditions _initFuture = _initializeSettings(); } - /// Ensures the provider is initialized. Call this before accessing settings - /// in contexts where you need the actual persisted values. Future ensureInitialized() => _initFuture ?? _initializeSettings(); - /// Whether the provider has finished loading persisted settings. - /// Synchronous getters below return defaults until this is true. bool get isReady => _isInitialized; - - /// Future that completes once persisted settings have been loaded. + bool get isInitialized => _isInitialized; Future get ready => _initFuture ?? Future.value(); Future _initializeSettings() async { if (_isInitialized) return; _settingsService = await SettingsService.getInstance(); - _readAllFromService(); _isInitialized = true; notifyListeners(); } - void _readAllFromService() { - final s = _settingsService!; - _libraryDensity = s.getLibraryDensity(); - _viewMode = s.getViewMode(); - _episodePosterMode = s.getEpisodePosterMode(); - _showHeroSection = s.getShowHeroSection(); - _useGlobalHubs = s.getUseGlobalHubs(); - _showServerNameOnHubs = s.getShowServerNameOnHubs(); - _groupLibrariesByServer = s.getGroupLibrariesByServer(); - _alwaysKeepSidebarOpen = s.getAlwaysKeepSidebarOpen(); - _showUnwatchedCount = s.getShowUnwatchedCount(); - _showEpisodeNumberOnCards = s.getShowEpisodeNumberOnCards(); - _hideSpoilers = s.getHideSpoilers(); - _showNavBarLabels = s.getShowNavBarLabels(); - _liveTvDefaultFavorites = s.getLiveTvDefaultFavorites(); - _autoHidePerformanceOverlay = s.getAutoHidePerformanceOverlay(); - _defaultQualityPreset = TranscodeQualityPreset.fromStorage(s.getDefaultQualityPreset()); - } - - /// Re-read all settings from SharedPreferences. Used after imports or resets - /// that change persisted settings outside this provider. + /// Re-read settings after an external mutation (import, reset). The provider + /// no longer caches values, so this just re-fetches the service instance and + /// notifies listeners. Future reload() async { _settingsService = await SettingsService.getInstance(); - _readAllFromService(); _isInitialized = true; notifyListeners(); } - /// Whether the provider has completed initialization - bool get isInitialized => _isInitialized; + T _read(Pref pref, T fallback) => _isInitialized ? _settingsService!.read(pref) : fallback; - int get libraryDensity => _libraryDensity; - - ViewMode get viewMode => _viewMode; - - EpisodePosterMode get episodePosterMode => _episodePosterMode; - - bool get showHeroSection => _showHeroSection; - - bool get useGlobalHubs => _useGlobalHubs; - - bool get showServerNameOnHubs => _showServerNameOnHubs; - - bool get groupLibrariesByServer => _groupLibrariesByServer; - - bool get alwaysKeepSidebarOpen => _alwaysKeepSidebarOpen; - - bool get showUnwatchedCount => _showUnwatchedCount; - - bool get showEpisodeNumberOnCards => _showEpisodeNumberOnCards; - - bool get hideSpoilers => _hideSpoilers; - - bool get showNavBarLabels => _showNavBarLabels; - - bool get liveTvDefaultFavorites => _liveTvDefaultFavorites; - - bool get autoHidePerformanceOverlay => _autoHidePerformanceOverlay; - - TranscodeQualityPreset get defaultQualityPreset => _defaultQualityPreset; - - /// Helper to update a setting: ensures init, deduplicates, persists, notifies. - Future _updateSetting({ - required T current, - required T value, - required void Function(T) setLocal, - required Future Function(T) persist, - }) async { + Future _set(Pref pref, T value) async { if (!_isInitialized) await _initializeSettings(); - if (current != value) { - setLocal(value); - await persist(value); - notifyListeners(); - } + if (_settingsService!.read(pref) == value) return; + await _settingsService!.write(pref, value); + notifyListeners(); } - Future setLibraryDensity(int density) => _updateSetting( - current: _libraryDensity, - value: density.clamp(LibraryDensity.min, LibraryDensity.max), - setLocal: (v) => _libraryDensity = v, - persist: _settingsService!.setLibraryDensity, - ); + int get libraryDensity => _read(SettingsService.libraryDensity, LibraryDensity.defaultValue); + ViewMode get viewMode => _read(SettingsService.viewMode, ViewMode.grid); + EpisodePosterMode get episodePosterMode => + _read(SettingsService.episodePosterMode, EpisodePosterMode.episodeThumbnail); + bool get showHeroSection => _read(SettingsService.showHeroSection, true); + bool get useGlobalHubs => _read(SettingsService.useGlobalHubs, true); + bool get showServerNameOnHubs => _read(SettingsService.showServerNameOnHubs, false); + bool get groupLibrariesByServer => _read(SettingsService.groupLibrariesByServer, true); + bool get alwaysKeepSidebarOpen => _read(SettingsService.alwaysKeepSidebarOpen, false); + bool get showUnwatchedCount => _read(SettingsService.showUnwatchedCount, true); + bool get showEpisodeNumberOnCards => _read(SettingsService.showEpisodeNumberOnCards, true); + bool get hideSpoilers => _read(SettingsService.hideSpoilers, false); + bool get showNavBarLabels => _read(SettingsService.showNavBarLabels, true); + bool get liveTvDefaultFavorites => _read(SettingsService.liveTvDefaultFavorites, false); + bool get autoHidePerformanceOverlay => _read(SettingsService.autoHidePerformanceOverlay, true); + TranscodeQualityPreset get defaultQualityPreset => + TranscodeQualityPreset.fromStorage(_read(SettingsService.defaultQualityPreset, 'original')); - Future setViewMode(ViewMode mode) => _updateSetting( - current: _viewMode, - value: mode, - setLocal: (v) => _viewMode = v, - persist: _settingsService!.setViewMode, - ); - - Future setEpisodePosterMode(EpisodePosterMode mode) => _updateSetting( - current: _episodePosterMode, - value: mode, - setLocal: (v) => _episodePosterMode = v, - persist: _settingsService!.setEpisodePosterMode, - ); - - Future setShowHeroSection(bool value) => _updateSetting( - current: _showHeroSection, - value: value, - setLocal: (v) => _showHeroSection = v, - persist: _settingsService!.setShowHeroSection, - ); - - Future setUseGlobalHubs(bool value) => _updateSetting( - current: _useGlobalHubs, - value: value, - setLocal: (v) => _useGlobalHubs = v, - persist: _settingsService!.setUseGlobalHubs, - ); - - Future setShowServerNameOnHubs(bool value) => _updateSetting( - current: _showServerNameOnHubs, - value: value, - setLocal: (v) => _showServerNameOnHubs = v, - persist: _settingsService!.setShowServerNameOnHubs, - ); - - Future setGroupLibrariesByServer(bool value) => _updateSetting( - current: _groupLibrariesByServer, - value: value, - setLocal: (v) => _groupLibrariesByServer = v, - persist: _settingsService!.setGroupLibrariesByServer, - ); - - Future setAlwaysKeepSidebarOpen(bool value) => _updateSetting( - current: _alwaysKeepSidebarOpen, - value: value, - setLocal: (v) => _alwaysKeepSidebarOpen = v, - persist: _settingsService!.setAlwaysKeepSidebarOpen, - ); - - Future setShowUnwatchedCount(bool value) => _updateSetting( - current: _showUnwatchedCount, - value: value, - setLocal: (v) => _showUnwatchedCount = v, - persist: _settingsService!.setShowUnwatchedCount, - ); - - Future setShowEpisodeNumberOnCards(bool value) => _updateSetting( - current: _showEpisodeNumberOnCards, - value: value, - setLocal: (v) => _showEpisodeNumberOnCards = v, - persist: _settingsService!.setShowEpisodeNumberOnCards, - ); - - Future setHideSpoilers(bool value) => _updateSetting( - current: _hideSpoilers, - value: value, - setLocal: (v) => _hideSpoilers = v, - persist: _settingsService!.setHideSpoilers, - ); - - Future setShowNavBarLabels(bool value) => _updateSetting( - current: _showNavBarLabels, - value: value, - setLocal: (v) => _showNavBarLabels = v, - persist: _settingsService!.setShowNavBarLabels, - ); - - Future setLiveTvDefaultFavorites(bool value) => _updateSetting( - current: _liveTvDefaultFavorites, - value: value, - setLocal: (v) => _liveTvDefaultFavorites = v, - persist: _settingsService!.setLiveTvDefaultFavorites, - ); - - Future setAutoHidePerformanceOverlay(bool value) => _updateSetting( - current: _autoHidePerformanceOverlay, - value: value, - setLocal: (v) => _autoHidePerformanceOverlay = v, - persist: _settingsService!.setAutoHidePerformanceOverlay, - ); - - Future setDefaultQualityPreset(TranscodeQualityPreset preset) => _updateSetting( - current: _defaultQualityPreset, - value: preset, - setLocal: (v) => _defaultQualityPreset = v, - persist: (v) => _settingsService!.setDefaultQualityPreset(v.storageKey), - ); + Future setLibraryDensity(int density) => + _set(SettingsService.libraryDensity, density.clamp(LibraryDensity.min, LibraryDensity.max)); + Future setViewMode(ViewMode mode) => _set(SettingsService.viewMode, mode); + Future setEpisodePosterMode(EpisodePosterMode mode) => _set(SettingsService.episodePosterMode, mode); + Future setShowHeroSection(bool value) => _set(SettingsService.showHeroSection, value); + Future setUseGlobalHubs(bool value) => _set(SettingsService.useGlobalHubs, value); + Future setShowServerNameOnHubs(bool value) => _set(SettingsService.showServerNameOnHubs, value); + Future setGroupLibrariesByServer(bool value) => _set(SettingsService.groupLibrariesByServer, value); + Future setAlwaysKeepSidebarOpen(bool value) => _set(SettingsService.alwaysKeepSidebarOpen, value); + Future setShowUnwatchedCount(bool value) => _set(SettingsService.showUnwatchedCount, value); + Future setShowEpisodeNumberOnCards(bool value) => _set(SettingsService.showEpisodeNumberOnCards, value); + Future setHideSpoilers(bool value) => _set(SettingsService.hideSpoilers, value); + Future setShowNavBarLabels(bool value) => _set(SettingsService.showNavBarLabels, value); + Future setLiveTvDefaultFavorites(bool value) => _set(SettingsService.liveTvDefaultFavorites, value); + Future setAutoHidePerformanceOverlay(bool value) => _set(SettingsService.autoHidePerformanceOverlay, value); + Future setDefaultQualityPreset(TranscodeQualityPreset preset) => + _set(SettingsService.defaultQualityPreset, preset.storageKey); } diff --git a/lib/providers/shader_provider.dart b/lib/providers/shader_provider.dart index 55d96610..c5db8677 100644 --- a/lib/providers/shader_provider.dart +++ b/lib/providers/shader_provider.dart @@ -23,10 +23,10 @@ class ShaderProvider extends ChangeNotifier { _settingsService = await SettingsService.getInstance(); // Load custom presets from storage - final customData = _settingsService.getCustomShaderPresets(); + final customData = _settingsService.read(SettingsService.customShaderPresets); _customPresets = customData.map((json) => ShaderPreset.fromJson(json)).toList(); - final presetId = _settingsService.getGlobalShaderPreset(); + final presetId = _settingsService.read(SettingsService.globalShaderPreset); _savedPreset = findPresetById(presetId) ?? ShaderPreset.none; _currentPreset = _savedPreset; @@ -62,7 +62,7 @@ class ShaderProvider extends ChangeNotifier { Future setPreset(ShaderPreset preset) async { _savedPreset = preset; _currentPreset = preset; - await _settingsService.setGlobalShaderPreset(preset.id); + await _settingsService.write(SettingsService.globalShaderPreset, preset.id); notifyListeners(); } @@ -100,7 +100,7 @@ class ShaderProvider extends ChangeNotifier { if (_currentPreset.id == preset.id || _savedPreset.id == preset.id) { _savedPreset = ShaderPreset.none; _currentPreset = ShaderPreset.none; - await _settingsService.setGlobalShaderPreset(ShaderPreset.none.id); + await _settingsService.write(SettingsService.globalShaderPreset, ShaderPreset.none.id); } notifyListeners(); @@ -108,7 +108,7 @@ class ShaderProvider extends ChangeNotifier { Future _saveCustomPresets() async { final data = _customPresets.map((p) => p.toJson()).toList(); - await _settingsService.setCustomShaderPresets(data); + await _settingsService.write(SettingsService.customShaderPresets, data); } /// Reset to default (no shaders) diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 0361b1e7..115b8397 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -33,7 +33,7 @@ class ThemeProvider extends ChangeNotifier { Future _initializeSettings() async { _settingsService = await settings.SettingsService.getInstance(); - _themeMode = _settingsService.getThemeMode(); + _themeMode = _settingsService.read(settings.SettingsService.themeMode); _updateSplashTheme(_themeMode); notifyListeners(); } @@ -79,7 +79,7 @@ class ThemeProvider extends ChangeNotifier { Future setThemeMode(settings.ThemeMode mode) async { if (_themeMode != mode) { _themeMode = mode; - await _settingsService.setThemeMode(mode); + await _settingsService.write(settings.SettingsService.themeMode, mode); _updateSplashTheme(mode); notifyListeners(); } @@ -89,7 +89,7 @@ class ThemeProvider extends ChangeNotifier { /// resets that change persisted settings outside this provider. Future reload() async { _settingsService = await settings.SettingsService.getInstance(); - final mode = _settingsService.getThemeMode(); + final mode = _settingsService.read(settings.SettingsService.themeMode); _themeMode = mode; _updateSplashTheme(mode); notifyListeners(); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 9ad8add5..1beee3cb 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -199,7 +199,8 @@ class _MainScreenState extends State with RouteAware, WindowListener final needsInitial = userProfileProvider.needsInitialProfileSelection; final settingsService = await SettingsService.getInstance(); if (!mounted) return; - final requireOnOpen = settingsService.getRequireProfileSelectionOnOpen() && userProfileProvider.hasMultipleUsers; + final requireOnOpen = + settingsService.read(SettingsService.requireProfileSelectionOnOpen) && userProfileProvider.hasMultipleUsers; if (!needsInitial && !requireOnOpen) return; @@ -214,7 +215,7 @@ class _MainScreenState extends State with RouteAware, WindowListener if (!mounted) return; final settingsService = await SettingsService.getInstance(); - if (!settingsService.getAutoCheckUpdatesOnStartup()) return; + if (!settingsService.read(SettingsService.autoCheckUpdatesOnStartup)) return; // Native updater (Sparkle/WinSparkle) handles everything — skip Flutter dialog if (UpdateService.useNativeUpdater) { @@ -449,7 +450,7 @@ class _MainScreenState extends State with RouteAware, WindowListener Future _autoStartCompanionRemoteServer(CompanionRemoteProvider companionRemote) async { try { final settings = await SettingsService.getInstance(); - if (!settings.getEnableCompanionRemoteServer()) return; + if (!settings.read(SettingsService.enableCompanionRemoteServer)) return; if (!mounted) return; final home = context.read().home; @@ -510,7 +511,7 @@ class _MainScreenState extends State with RouteAware, WindowListener Future _showProfileSelectionOnResume() async { final settingsService = await SettingsService.getInstance(); - if (!settingsService.getRequireProfileSelectionOnOpen()) return; + if (!settingsService.read(SettingsService.requireProfileSelectionOnOpen)) return; if (!mounted) return; final userProfileProvider = context.read(); @@ -682,7 +683,7 @@ class _MainScreenState extends State with RouteAware, WindowListener return handleBackKeyAction(event, () async { if (PlatformDetector.isTV()) { final settings = await SettingsService.getInstance(); - if (settings.getConfirmExitOnBack() && mounted) { + if (settings.read(SettingsService.confirmExitOnBack) && mounted) { final result = await showConfirmDialogWithCheckbox( context, title: t.common.exitConfirmTitle, @@ -691,7 +692,7 @@ class _MainScreenState extends State with RouteAware, WindowListener checkboxLabel: t.common.dontAskAgain, ); if (result.checked) { - await settings.setConfirmExitOnBack(false); + await settings.write(SettingsService.confirmExitOnBack, false); } if (!result.confirmed) return; } diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index d0b6eb3f..faca33d3 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -26,9 +26,6 @@ class AppearanceSettingsScreen extends StatefulWidget { class _AppearanceSettingsScreenState extends State { late settings.SettingsService _settingsService; bool _isLoading = true; - bool _requireProfileSelectionOnOpen = false; - bool _confirmExitOnBack = true; - bool _forceTvMode = false; @override void initState() { @@ -39,12 +36,7 @@ class _AppearanceSettingsScreenState extends State { Future _loadSettings() async { _settingsService = await settings.SettingsService.getInstance(); if (!mounted) return; - setState(() { - _requireProfileSelectionOnOpen = _settingsService.getRequireProfileSelectionOnOpen(); - _confirmExitOnBack = _settingsService.getConfirmExitOnBack(); - _forceTvMode = _settingsService.getForceTvMode(); - _isLoading = false; - }); + setState(() => _isLoading = false); } @override @@ -135,7 +127,7 @@ class _AppearanceSettingsScreenState extends State { currentValue: LocaleSettings.currentLocale, ); if (value != null) { - await _settingsService.setAppLocale(value); + await _settingsService.write(settings.SettingsService.appLocale, value); LocaleSettings.setLocale(value); _restartApp(); } @@ -333,45 +325,55 @@ class _AppearanceSettingsScreenState extends State { return Consumer( builder: (context, userProfileProvider, child) { if (!userProfileProvider.hasMultipleUsers) return const SizedBox.shrink(); - return SwitchListTile( - secondary: const AppIcon(Symbols.person_rounded, fill: 1), - title: Text(t.settings.requireProfileSelectionOnOpen), - subtitle: Text(t.settings.requireProfileSelectionOnOpenDescription), - value: _requireProfileSelectionOnOpen, - onChanged: (value) async { - setState(() => _requireProfileSelectionOnOpen = value); - await _settingsService.setRequireProfileSelectionOnOpen(value); - }, + return _buildListenableSwitch( + icon: Symbols.person_rounded, + title: t.settings.requireProfileSelectionOnOpen, + subtitle: t.settings.requireProfileSelectionOnOpenDescription, + pref: settings.SettingsService.requireProfileSelectionOnOpen, ); }, ); } - Widget _buildConfirmExitOnBack() { - return SwitchListTile( - secondary: const AppIcon(Symbols.exit_to_app_rounded, fill: 1), - title: Text(t.settings.confirmExitOnBack), - subtitle: Text(t.settings.confirmExitOnBackDescription), - value: _confirmExitOnBack, - onChanged: (value) async { - setState(() => _confirmExitOnBack = value); - await _settingsService.setConfirmExitOnBack(value); - }, - ); - } + Widget _buildConfirmExitOnBack() => _buildListenableSwitch( + icon: Symbols.exit_to_app_rounded, + title: t.settings.confirmExitOnBack, + subtitle: t.settings.confirmExitOnBackDescription, + pref: settings.SettingsService.confirmExitOnBack, + ); - Widget _buildForceTvMode() { - return SwitchListTile( - secondary: const AppIcon(Symbols.tv_rounded, fill: 1), - title: Text(t.settings.forceTvMode), - subtitle: Text(t.settings.forceTvModeDescription), - value: _forceTvMode, - onChanged: (value) async { - setState(() => _forceTvMode = value); - await _settingsService.setForceTvMode(value); - TvDetectionService.setForceTVSync(value); - if (mounted) _restartApp(); - }, + Widget _buildForceTvMode() => _buildListenableSwitch( + icon: Symbols.tv_rounded, + title: t.settings.forceTvMode, + subtitle: t.settings.forceTvModeDescription, + pref: settings.SettingsService.forceTvMode, + onAfterWrite: (value) { + TvDetectionService.setForceTVSync(value); + if (mounted) _restartApp(); + }, + ); + + /// Generic bool toggle that listens to a [Pref] directly via [ValueNotifier]. + /// No local state; rebuilds across the app stay consistent. + Widget _buildListenableSwitch({ + required IconData icon, + required String title, + required String subtitle, + required settings.Pref pref, + void Function(bool)? onAfterWrite, + }) { + return ValueListenableBuilder( + valueListenable: _settingsService.listenable(pref), + builder: (_, value, _) => SwitchListTile( + secondary: AppIcon(icon, fill: 1), + title: Text(title), + subtitle: Text(subtitle), + value: value, + onChanged: (v) async { + await _settingsService.write(pref, v); + onAfterWrite?.call(v); + }, + ), ); } diff --git a/lib/screens/settings/external_player_screen.dart b/lib/screens/settings/external_player_screen.dart index 79b0d883..6d2067c3 100644 --- a/lib/screens/settings/external_player_screen.dart +++ b/lib/screens/settings/external_player_screen.dart @@ -37,9 +37,9 @@ class _ExternalPlayerScreenState extends State { if (!mounted) return; setState(() { - _useExternalPlayer = _settingsService.getUseExternalPlayer(); - _selectedPlayer = _settingsService.getSelectedExternalPlayer(); - _customPlayers = _settingsService.getCustomExternalPlayers(); + _useExternalPlayer = _settingsService.read(SettingsService.useExternalPlayer); + _selectedPlayer = _settingsService.read(SettingsService.selectedExternalPlayer); + _customPlayers = _settingsService.read(SettingsService.customExternalPlayers); _isLoading = false; }); } @@ -67,7 +67,7 @@ class _ExternalPlayerScreenState extends State { value: _useExternalPlayer, onChanged: (value) async { setState(() => _useExternalPlayer = value); - await _settingsService.setUseExternalPlayer(value); + await _settingsService.write(SettingsService.useExternalPlayer, value); }, ), if (_useExternalPlayer) ...[ @@ -132,7 +132,7 @@ class _ExternalPlayerScreenState extends State { ), onTap: () async { setState(() => _selectedPlayer = player); - await _settingsService.setSelectedExternalPlayer(player); + await _settingsService.write(SettingsService.selectedExternalPlayer, player); }, ); } @@ -142,7 +142,7 @@ class _ExternalPlayerScreenState extends State { if (!mounted) return; setState(() { _customPlayers.removeWhere((p) => p.id == player.id); - _selectedPlayer = _settingsService.getSelectedExternalPlayer(); + _selectedPlayer = _settingsService.read(SettingsService.selectedExternalPlayer); }); } @@ -255,7 +255,10 @@ class _ExternalPlayerScreenState extends State { type: selectedType, ); - await _settingsService.addCustomExternalPlayer(newPlayer); + await _settingsService.write(SettingsService.customExternalPlayers, [ + ..._settingsService.read(SettingsService.customExternalPlayers), + newPlayer, + ]); if (!mounted) return; setState(() { _customPlayers.add(newPlayer); diff --git a/lib/screens/settings/mpv_config_screen.dart b/lib/screens/settings/mpv_config_screen.dart index 6b7b1e63..ca57632b 100644 --- a/lib/screens/settings/mpv_config_screen.dart +++ b/lib/screens/settings/mpv_config_screen.dart @@ -47,14 +47,14 @@ class _MpvConfigScreenState extends State { if (!mounted) return; setState(() { - _textController.text = _settingsService.getMpvConfigText(); - _presets = _settingsService.getMpvPresets(); + _textController.text = _settingsService.read(SettingsService.mpvConfigText); + _presets = _settingsService.read(SettingsService.mpvPresets); _isLoading = false; }); } Future _saveText() async { - await _settingsService.setMpvConfigText(_textController.text); + await _settingsService.write(SettingsService.mpvConfigText, _textController.text); } Future _showSavePresetDialog() async { @@ -71,7 +71,7 @@ class _MpvConfigScreenState extends State { await _settingsService.saveMpvPreset(name.trim(), _textController.text); if (!mounted) return; setState(() { - _presets = _settingsService.getMpvPresets(); + _presets = _settingsService.read(SettingsService.mpvPresets); }); if (mounted) { @@ -84,7 +84,7 @@ class _MpvConfigScreenState extends State { await _settingsService.loadMpvPreset(preset.name); if (!mounted) return; setState(() { - _textController.text = _settingsService.getMpvConfigText(); + _textController.text = _settingsService.read(SettingsService.mpvConfigText); }); if (mounted) { @@ -103,7 +103,7 @@ class _MpvConfigScreenState extends State { await _settingsService.deleteMpvPreset(preset.name); if (!mounted) return; setState(() { - _presets = _settingsService.getMpvPresets(); + _presets = _settingsService.read(SettingsService.mpvPresets); }); if (mounted) { diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index 966430ff..4a0d6d1f 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -73,32 +73,34 @@ class _PlaybackSettingsScreenState extends State { if (!mounted) return; setState(() { - _enableHardwareDecoding = _settingsService.getEnableHardwareDecoding(); - _bufferSize = _settingsService.getBufferSize(); - _seekTimeSmall = _settingsService.getSeekTimeSmall(); - _seekTimeLarge = _settingsService.getSeekTimeLarge(); - _rewindOnResume = _settingsService.getRewindOnResume(); - _sleepTimerDuration = _settingsService.getSleepTimerDuration(); - _rememberTrackSelections = _settingsService.getRememberTrackSelections(); - _clickVideoTogglesPlayback = _settingsService.getClickVideoTogglesPlayback(); - _autoSkipIntro = _settingsService.getAutoSkipIntro(); - _autoSkipCredits = _settingsService.getAutoSkipCredits(); - _autoSkipDelay = _settingsService.getAutoSkipDelay(); - _introPattern = _settingsService.getIntroPattern(); - _creditsPattern = _settingsService.getCreditsPattern(); - _maxVolume = _settingsService.getMaxVolume(); - _enableDiscordRPC = _settingsService.getEnableDiscordRPC(); - _enableCompanionRemoteServer = _settingsService.getEnableCompanionRemoteServer(); - _autoPip = _settingsService.getAutoPip(); - _matchContentFrameRate = _settingsService.getMatchContentFrameRate(); - _matchRefreshRate = _settingsService.getMatchRefreshRate(); - _matchDynamicRange = _settingsService.getMatchDynamicRange(); - _displaySwitchDelay = _settingsService.getDisplaySwitchDelay(); - _tunneledPlayback = _settingsService.getTunneledPlayback(); - _useExoPlayer = _settingsService.getUseExoPlayer(); - _useExternalPlayer = _settingsService.getUseExternalPlayer(); - _selectedExternalPlayerName = _settingsService.getSelectedExternalPlayer().name; - _defaultQualityPreset = TranscodeQualityPreset.fromStorage(_settingsService.getDefaultQualityPreset()); + _enableHardwareDecoding = _settingsService.read(settings.SettingsService.enableHardwareDecoding); + _bufferSize = _settingsService.read(settings.SettingsService.bufferSize); + _seekTimeSmall = _settingsService.read(settings.SettingsService.seekTimeSmall); + _seekTimeLarge = _settingsService.read(settings.SettingsService.seekTimeLarge); + _rewindOnResume = _settingsService.read(settings.SettingsService.rewindOnResume); + _sleepTimerDuration = _settingsService.read(settings.SettingsService.sleepTimerDuration); + _rememberTrackSelections = _settingsService.read(settings.SettingsService.rememberTrackSelections); + _clickVideoTogglesPlayback = _settingsService.read(settings.SettingsService.clickVideoTogglesPlayback); + _autoSkipIntro = _settingsService.read(settings.SettingsService.autoSkipIntro); + _autoSkipCredits = _settingsService.read(settings.SettingsService.autoSkipCredits); + _autoSkipDelay = _settingsService.read(settings.SettingsService.autoSkipDelay); + _introPattern = _settingsService.read(settings.SettingsService.introPattern); + _creditsPattern = _settingsService.read(settings.SettingsService.creditsPattern); + _maxVolume = _settingsService.read(settings.SettingsService.maxVolume); + _enableDiscordRPC = _settingsService.read(settings.SettingsService.enableDiscordRPC); + _enableCompanionRemoteServer = _settingsService.read(settings.SettingsService.enableCompanionRemoteServer); + _autoPip = _settingsService.read(settings.SettingsService.autoPip); + _matchContentFrameRate = _settingsService.read(settings.SettingsService.matchContentFrameRate); + _matchRefreshRate = _settingsService.read(settings.SettingsService.matchRefreshRate); + _matchDynamicRange = _settingsService.read(settings.SettingsService.matchDynamicRange); + _displaySwitchDelay = _settingsService.read(settings.SettingsService.displaySwitchDelay); + _tunneledPlayback = _settingsService.read(settings.SettingsService.tunneledPlayback); + _useExoPlayer = _settingsService.read(settings.SettingsService.useExoPlayer); + _useExternalPlayer = _settingsService.read(settings.SettingsService.useExternalPlayer); + _selectedExternalPlayerName = _settingsService.read(settings.SettingsService.selectedExternalPlayer).name; + _defaultQualityPreset = TranscodeQualityPreset.fromStorage( + _settingsService.read(settings.SettingsService.defaultQualityPreset), + ); _isLoading = false; }); } @@ -182,7 +184,7 @@ class _PlaybackSettingsScreenState extends State { selected: _useExoPlayer, onChanged: (value) async { setState(() => _useExoPlayer = value); - await _settingsService.setUseExoPlayer(value); + await _settingsService.write(settings.SettingsService.useExoPlayer, value); }, ); } @@ -198,8 +200,8 @@ class _PlaybackSettingsScreenState extends State { final s = await settings.SettingsService.getInstance(); if (!mounted) return; setState(() { - _useExternalPlayer = s.getUseExternalPlayer(); - _selectedExternalPlayerName = s.getSelectedExternalPlayer().name; + _useExternalPlayer = s.read(settings.SettingsService.useExternalPlayer); + _selectedExternalPlayerName = s.read(settings.SettingsService.selectedExternalPlayer).name; }); }, ); @@ -213,7 +215,7 @@ class _PlaybackSettingsScreenState extends State { value: _enableHardwareDecoding, onChanged: (value) async { setState(() => _enableHardwareDecoding = value); - await _settingsService.setEnableHardwareDecoding(value); + await _settingsService.write(settings.SettingsService.enableHardwareDecoding, value); }, ); } @@ -226,7 +228,7 @@ class _PlaybackSettingsScreenState extends State { value: _autoPip, onChanged: (value) async { setState(() => _autoPip = value); - await _settingsService.setAutoPip(value); + await _settingsService.write(settings.SettingsService.autoPip, value); }, ); } @@ -239,7 +241,7 @@ class _PlaybackSettingsScreenState extends State { value: _matchContentFrameRate, onChanged: (value) async { setState(() => _matchContentFrameRate = value); - await _settingsService.setMatchContentFrameRate(value); + await _settingsService.write(settings.SettingsService.matchContentFrameRate, value); }, ); } @@ -252,7 +254,7 @@ class _PlaybackSettingsScreenState extends State { value: _matchRefreshRate, onChanged: (value) async { setState(() => _matchRefreshRate = value); - await _settingsService.setMatchRefreshRate(value); + await _settingsService.write(settings.SettingsService.matchRefreshRate, value); }, ); } @@ -265,7 +267,7 @@ class _PlaybackSettingsScreenState extends State { value: _matchDynamicRange, onChanged: (value) async { setState(() => _matchDynamicRange = value); - await _settingsService.setMatchDynamicRange(value); + await _settingsService.write(settings.SettingsService.matchDynamicRange, value); }, ); } @@ -286,7 +288,7 @@ class _PlaybackSettingsScreenState extends State { currentValue: _displaySwitchDelay, onSave: (value) async { setState(() => _displaySwitchDelay = value); - await _settingsService.setDisplaySwitchDelay(value); + await _settingsService.write(settings.SettingsService.displaySwitchDelay, value); }, ), ); @@ -300,7 +302,7 @@ class _PlaybackSettingsScreenState extends State { value: _tunneledPlayback, onChanged: (value) async { setState(() => _tunneledPlayback = value); - await _settingsService.setTunneledPlayback(value); + await _settingsService.write(settings.SettingsService.tunneledPlayback, value); }, ); } @@ -323,7 +325,7 @@ class _PlaybackSettingsScreenState extends State { if (value != null) { setState(() { _defaultQualityPreset = value; - _settingsService.setDefaultQualityPreset(value.storageKey); + _settingsService.write(settings.SettingsService.defaultQualityPreset, value.storageKey); }); } }, @@ -351,7 +353,7 @@ class _PlaybackSettingsScreenState extends State { if (value != null) { setState(() { _bufferSize = value; - _settingsService.setBufferSize(value); + _settingsService.write(settings.SettingsService.bufferSize, value); }); if (Platform.isAndroid && value > 0) { final heapMB = await PlayerAndroid.getHeapSize(); @@ -409,7 +411,7 @@ class _PlaybackSettingsScreenState extends State { onSave: (value) async { setState(() { _seekTimeSmall = value; - _settingsService.setSeekTimeSmall(value); + _settingsService.write(settings.SettingsService.seekTimeSmall, value); }); await _keyboardService?.refreshFromStorage(); }, @@ -434,7 +436,7 @@ class _PlaybackSettingsScreenState extends State { onSave: (value) async { setState(() { _seekTimeLarge = value; - _settingsService.setSeekTimeLarge(value); + _settingsService.write(settings.SettingsService.seekTimeLarge, value); }); await _keyboardService?.refreshFromStorage(); }, @@ -459,7 +461,7 @@ class _PlaybackSettingsScreenState extends State { onSave: (value) async { setState(() { _rewindOnResume = value; - _settingsService.setRewindOnResume(value); + _settingsService.write(settings.SettingsService.rewindOnResume, value); }); }, ), @@ -482,7 +484,7 @@ class _PlaybackSettingsScreenState extends State { currentValue: _sleepTimerDuration, onSave: (value) async { setState(() => _sleepTimerDuration = value); - await _settingsService.setSleepTimerDuration(value); + await _settingsService.write(settings.SettingsService.sleepTimerDuration, value); }, ), ); @@ -504,7 +506,7 @@ class _PlaybackSettingsScreenState extends State { currentValue: _maxVolume, onSave: (value) async { setState(() => _maxVolume = value); - await _settingsService.setMaxVolume(value); + await _settingsService.write(settings.SettingsService.maxVolume, value); }, ), ); @@ -520,7 +522,7 @@ class _PlaybackSettingsScreenState extends State { value: _enableDiscordRPC, onChanged: (value) async { setState(() => _enableDiscordRPC = value); - await _settingsService.setEnableDiscordRPC(value); + await _settingsService.write(settings.SettingsService.enableDiscordRPC, value); await DiscordRPCService.instance.setEnabled(value); }, ); @@ -534,7 +536,7 @@ class _PlaybackSettingsScreenState extends State { value: _enableCompanionRemoteServer, onChanged: (value) async { setState(() => _enableCompanionRemoteServer = value); - await _settingsService.setEnableCompanionRemoteServer(value); + await _settingsService.write(settings.SettingsService.enableCompanionRemoteServer, value); }, ); } @@ -547,7 +549,7 @@ class _PlaybackSettingsScreenState extends State { value: _rememberTrackSelections, onChanged: (value) async { setState(() => _rememberTrackSelections = value); - await _settingsService.setRememberTrackSelections(value); + await _settingsService.write(settings.SettingsService.rememberTrackSelections, value); }, ); } @@ -560,7 +562,7 @@ class _PlaybackSettingsScreenState extends State { value: _clickVideoTogglesPlayback, onChanged: (value) async { setState(() => _clickVideoTogglesPlayback = value); - await _settingsService.setClickVideoTogglesPlayback(value); + await _settingsService.write(settings.SettingsService.clickVideoTogglesPlayback, value); }, ); } @@ -575,7 +577,7 @@ class _PlaybackSettingsScreenState extends State { value: _autoSkipIntro, onChanged: (value) async { setState(() => _autoSkipIntro = value); - await _settingsService.setAutoSkipIntro(value); + await _settingsService.write(settings.SettingsService.autoSkipIntro, value); }, ); } @@ -588,7 +590,7 @@ class _PlaybackSettingsScreenState extends State { value: _autoSkipCredits, onChanged: (value) async { setState(() => _autoSkipCredits = value); - await _settingsService.setAutoSkipCredits(value); + await _settingsService.write(settings.SettingsService.autoSkipCredits, value); }, ); } @@ -609,7 +611,7 @@ class _PlaybackSettingsScreenState extends State { currentValue: _autoSkipDelay, onSave: (value) async { setState(() => _autoSkipDelay = value); - await _settingsService.setAutoSkipDelay(value); + await _settingsService.write(settings.SettingsService.autoSkipDelay, value); }, ), ); @@ -628,7 +630,7 @@ class _PlaybackSettingsScreenState extends State { defaultValue: settings.SettingsService.defaultIntroPattern, onSave: (value) async { setState(() => _introPattern = value); - await _settingsService.setIntroPattern(value); + await _settingsService.write(settings.SettingsService.introPattern, value); }, ), ); @@ -647,7 +649,7 @@ class _PlaybackSettingsScreenState extends State { defaultValue: settings.SettingsService.defaultCreditsPattern, onSave: (value) async { setState(() => _creditsPattern = value); - await _settingsService.setCreditsPattern(value); + await _settingsService.write(settings.SettingsService.creditsPattern, value); }, ), ); diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index c85c727b..8350a06a 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -136,13 +136,13 @@ class _SettingsScreenState extends State with FocusableTab { if (!mounted) return; setState(() { - _crashReporting = _settingsService.getCrashReporting(); - _enableDebugLogging = _settingsService.getEnableDebugLogging(); - _downloadOnWifiOnly = _settingsService.getDownloadOnWifiOnly(); - _autoRemoveWatchedDownloads = _settingsService.getAutoRemoveWatchedDownloads(); - _autoCheckUpdatesOnStartup = _settingsService.getAutoCheckUpdatesOnStartup(); - _videoPlayerNavigationEnabled = _settingsService.getVideoPlayerNavigationEnabled(); - _customRelayUrl = _settingsService.getCustomRelayUrl(); + _crashReporting = _settingsService.read(settings.SettingsService.crashReporting); + _enableDebugLogging = _settingsService.read(settings.SettingsService.enableDebugLogging); + _downloadOnWifiOnly = _settingsService.read(settings.SettingsService.downloadOnWifiOnly); + _autoRemoveWatchedDownloads = _settingsService.read(settings.SettingsService.autoRemoveWatchedDownloads); + _autoCheckUpdatesOnStartup = _settingsService.read(settings.SettingsService.autoCheckUpdatesOnStartup); + _videoPlayerNavigationEnabled = _settingsService.read(settings.SettingsService.videoPlayerNavigationEnabled); + _customRelayUrl = _settingsService.read(settings.SettingsService.customRelayUrl); _isLoading = false; }); } @@ -312,7 +312,7 @@ class _SettingsScreenState extends State with FocusableTab { value: _downloadOnWifiOnly, onChanged: (value) async { setState(() => _downloadOnWifiOnly = value); - await _settingsService.setDownloadOnWifiOnly(value); + await _settingsService.write(settings.SettingsService.downloadOnWifiOnly, value); }, ), SwitchListTile( @@ -323,7 +323,7 @@ class _SettingsScreenState extends State with FocusableTab { value: _autoRemoveWatchedDownloads, onChanged: (value) async { setState(() => _autoRemoveWatchedDownloads = value); - await _settingsService.setAutoRemoveWatchedDownloads(value); + await _settingsService.write(settings.SettingsService.autoRemoveWatchedDownloads, value); }, ), ], @@ -358,7 +358,7 @@ class _SettingsScreenState extends State with FocusableTab { value: _videoPlayerNavigationEnabled, onChanged: (value) async { setState(() => _videoPlayerNavigationEnabled = value); - await _settingsService.setVideoPlayerNavigationEnabled(value); + await _settingsService.write(settings.SettingsService.videoPlayerNavigationEnabled, value); }, ), ], @@ -386,7 +386,7 @@ class _SettingsScreenState extends State with FocusableTab { value: _crashReporting, onChanged: (value) async { setState(() => _crashReporting = value); - await _settingsService.setCrashReporting(value); + await _settingsService.write(settings.SettingsService.crashReporting, value); }, ), SwitchListTile( @@ -397,7 +397,7 @@ class _SettingsScreenState extends State with FocusableTab { value: _enableDebugLogging, onChanged: (value) async { setState(() => _enableDebugLogging = value); - await _settingsService.setEnableDebugLogging(value); + await _settingsService.write(settings.SettingsService.enableDebugLogging, value); }, ), ListTile( @@ -486,7 +486,7 @@ class _SettingsScreenState extends State with FocusableTab { value: _autoCheckUpdatesOnStartup, onChanged: (value) async { setState(() => _autoCheckUpdatesOnStartup = value); - await _settingsService.setAutoCheckUpdatesOnStartup(value); + await _settingsService.write(settings.SettingsService.autoCheckUpdatesOnStartup, value); }, ); } @@ -625,7 +625,8 @@ class _SettingsScreenState extends State with FocusableTab { } } - await _settingsService.setCustomDownloadPath(selectedPath, type: pathType); + await _settingsService.write(settings.SettingsService.customDownloadPath, selectedPath); + await _settingsService.write(settings.SettingsService.customDownloadPathType, pathType); await DownloadStorageService.instance.refreshCustomPath(); if (mounted) { @@ -642,7 +643,8 @@ class _SettingsScreenState extends State with FocusableTab { } Future _resetDownloadLocation() async { - await _settingsService.setCustomDownloadPath(null); + await _settingsService.write(settings.SettingsService.customDownloadPath, null); + await _settingsService.write(settings.SettingsService.customDownloadPathType, null); await DownloadStorageService.instance.refreshCustomPath(); if (mounted) { @@ -672,7 +674,7 @@ class _SettingsScreenState extends State with FocusableTab { DialogActionButton( onPressed: () async { controller.clear(); - await _settingsService.setCustomRelayUrl(null); + await _settingsService.write(settings.SettingsService.customRelayUrl, null); if (mounted) setState(() => _customRelayUrl = null); if (dialogContext.mounted) Navigator.pop(dialogContext); }, @@ -683,7 +685,7 @@ class _SettingsScreenState extends State with FocusableTab { focusNode: saveFocusNode, onPressed: () async { final url = controller.text.trim().isEmpty ? null : controller.text.trim(); - await _settingsService.setCustomRelayUrl(url); + await _settingsService.write(settings.SettingsService.customRelayUrl, url); if (mounted) setState(() => _customRelayUrl = url); if (dialogContext.mounted) Navigator.pop(dialogContext); }, @@ -764,7 +766,7 @@ class _SettingsScreenState extends State with FocusableTab { if (!mounted) return; if (result == null) return; // user cancelled file picker - LocaleSettings.setLocale(_settingsService.getAppLocale()); + LocaleSettings.setLocale(_settingsService.read(settings.SettingsService.appLocale)); await Future.wait([ themeProvider.reload(), settingsProvider.reload(), diff --git a/lib/screens/settings/subtitle_styling_screen.dart b/lib/screens/settings/subtitle_styling_screen.dart index 9b521974..ea14ccea 100644 --- a/lib/screens/settings/subtitle_styling_screen.dart +++ b/lib/screens/settings/subtitle_styling_screen.dart @@ -44,16 +44,16 @@ class _SubtitleStylingScreenState extends State { if (!mounted) return; setState(() { - _fontSize = _settingsService.getSubtitleFontSize(); - _textColor = _settingsService.getSubtitleTextColor(); - _borderSize = _settingsService.getSubtitleBorderSize(); - _borderColor = _settingsService.getSubtitleBorderColor(); - _backgroundColor = _settingsService.getSubtitleBackgroundColor(); - _backgroundOpacity = _settingsService.getSubtitleBackgroundOpacity(); - _subtitlePosition = _settingsService.getSubtitlePosition(); - _assOverride = _settingsService.getSubAssOverride(); - _bold = _settingsService.getSubtitleBold(); - _italic = _settingsService.getSubtitleItalic(); + _fontSize = _settingsService.read(SettingsService.subtitleFontSize); + _textColor = _settingsService.read(SettingsService.subtitleTextColor); + _borderSize = _settingsService.read(SettingsService.subtitleBorderSize); + _borderColor = _settingsService.read(SettingsService.subtitleBorderColor); + _backgroundColor = _settingsService.read(SettingsService.subtitleBackgroundColor); + _backgroundOpacity = _settingsService.read(SettingsService.subtitleBackgroundOpacity); + _subtitlePosition = _settingsService.read(SettingsService.subtitlePosition); + _assOverride = _settingsService.read(SettingsService.subAssOverride); + _bold = _settingsService.read(SettingsService.subtitleBold); + _italic = _settingsService.read(SettingsService.subtitleItalic); _isLoading = false; }); } @@ -200,7 +200,7 @@ class _SubtitleStylingScreenState extends State { ); if (value != null) { setState(() => _assOverride = value); - await _settingsService.setSubAssOverride(value); + await _settingsService.write(SettingsService.subAssOverride, value); } }, ), @@ -219,7 +219,7 @@ class _SubtitleStylingScreenState extends State { currentValue: _fontSize, onSave: (value) async { setState(() => _fontSize = value); - await _settingsService.setSubtitleFontSize(value); + await _settingsService.write(SettingsService.subtitleFontSize, value); }, ), ), @@ -238,7 +238,7 @@ class _SubtitleStylingScreenState extends State { trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () => _openColorPicker(t.subtitlingStyling.textColor, _textColor, (color) { setState(() => _textColor = color); - _settingsService.setSubtitleTextColor(color); + _settingsService.write(SettingsService.subtitleTextColor, color); }), ), ListTile( @@ -256,7 +256,7 @@ class _SubtitleStylingScreenState extends State { currentValue: _subtitlePosition, onSave: (value) async { setState(() => _subtitlePosition = value); - await _settingsService.setSubtitlePosition(value); + await _settingsService.write(SettingsService.subtitlePosition, value); }, ), ), @@ -266,7 +266,7 @@ class _SubtitleStylingScreenState extends State { value: _bold, onChanged: (value) async { setState(() => _bold = value); - await _settingsService.setSubtitleBold(value); + await _settingsService.write(SettingsService.subtitleBold, value); }, ), SwitchListTile( @@ -275,7 +275,7 @@ class _SubtitleStylingScreenState extends State { value: _italic, onChanged: (value) async { setState(() => _italic = value); - await _settingsService.setSubtitleItalic(value); + await _settingsService.write(SettingsService.subtitleItalic, value); }, ), @@ -296,7 +296,7 @@ class _SubtitleStylingScreenState extends State { currentValue: _borderSize, onSave: (value) async { setState(() => _borderSize = value); - await _settingsService.setSubtitleBorderSize(value); + await _settingsService.write(SettingsService.subtitleBorderSize, value); }, ), ), @@ -315,7 +315,7 @@ class _SubtitleStylingScreenState extends State { trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () => _openColorPicker(t.subtitlingStyling.borderColor, _borderColor, (color) { setState(() => _borderColor = color); - _settingsService.setSubtitleBorderColor(color); + _settingsService.write(SettingsService.subtitleBorderColor, color); }), ), @@ -336,7 +336,7 @@ class _SubtitleStylingScreenState extends State { currentValue: _backgroundOpacity, onSave: (value) async { setState(() => _backgroundOpacity = value); - await _settingsService.setSubtitleBackgroundOpacity(value); + await _settingsService.write(SettingsService.subtitleBackgroundOpacity, value); }, ), ), @@ -355,7 +355,7 @@ class _SubtitleStylingScreenState extends State { trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () => _openColorPicker(t.subtitlingStyling.backgroundColor, _backgroundColor, (color) { setState(() => _backgroundColor = color); - _settingsService.setSubtitleBackgroundColor(color); + _settingsService.write(SettingsService.subtitleBackgroundColor, color); }), ), const SizedBox(height: 24), diff --git a/lib/screens/settings/tracker_library_filter_screen.dart b/lib/screens/settings/tracker_library_filter_screen.dart index c7399762..a18ec48c 100644 --- a/lib/screens/settings/tracker_library_filter_screen.dart +++ b/lib/screens/settings/tracker_library_filter_screen.dart @@ -23,8 +23,8 @@ class TrackerLibraryFilterScreen extends StatefulWidget { /// Human-readable summary of the current filter state for [service], used as /// the subtitle on the parent settings screen's "Library filter" tile. static String subtitleFor(SettingsService settings, TrackerService service) { - final mode = settings.getTrackerLibraryFilterMode(service); - final ids = settings.getTrackerLibraryFilterIds(service); + final mode = settings.read(SettingsService.trackerFilterModePref(service)); + final ids = settings.read(SettingsService.trackerFilterIdsPref(service)).toSet(); if (ids.isEmpty) { return mode == TrackerLibraryFilterMode.blacklist ? t.trackers.libraryFilter.subtitleAllSyncing @@ -57,10 +57,10 @@ class _TrackerLibraryFilterScreenState extends State if (!mounted) return; setState(() { _settings = s; - _mode = s.getTrackerLibraryFilterMode(widget.service); + _mode = s.read(SettingsService.trackerFilterModePref(widget.service)); _selectedIds ..clear() - ..addAll(s.getTrackerLibraryFilterIds(widget.service)); + ..addAll(s.read(SettingsService.trackerFilterIdsPref(widget.service)).toSet()); _loaded = true; }); } @@ -68,7 +68,7 @@ class _TrackerLibraryFilterScreenState extends State Future _setMode(TrackerLibraryFilterMode mode) async { if (mode == _mode) return; setState(() => _mode = mode); - await _settings.setTrackerLibraryFilterMode(widget.service, mode); + await _settings.write(SettingsService.trackerFilterModePref(widget.service), mode); } Future _toggleLibrary(String globalKey, bool value) async { @@ -79,7 +79,7 @@ class _TrackerLibraryFilterScreenState extends State _selectedIds.remove(globalKey); } }); - await _settings.setTrackerLibraryFilterIds(widget.service, _selectedIds); + await _settings.write(SettingsService.trackerFilterIdsPref(widget.service), _selectedIds.toList()); } @override diff --git a/lib/screens/settings/tracker_settings_screen.dart b/lib/screens/settings/tracker_settings_screen.dart index d77f4f20..1983f625 100644 --- a/lib/screens/settings/tracker_settings_screen.dart +++ b/lib/screens/settings/tracker_settings_screen.dart @@ -88,9 +88,9 @@ class TrackerConfig { displayName: t.trackers.services.mal, isConnected: (a) => a.isMalConnected, username: (a) => a.malUsername, - readScrobbleEnabled: (s) => s.getEnableMalScrobble(), + readScrobbleEnabled: (s) => s.read(SettingsService.enableMalScrobble), setScrobbleEnabled: (s, v) async { - await s.setEnableMalScrobble(v); + await s.write(SettingsService.enableMalScrobble, v); await MalTracker.instance.setEnabled(v); }, disconnect: (a) => a.disconnectMal(), @@ -101,9 +101,9 @@ class TrackerConfig { displayName: t.trackers.services.anilist, isConnected: (a) => a.isAnilistConnected, username: (a) => a.anilistUsername, - readScrobbleEnabled: (s) => s.getEnableAnilistScrobble(), + readScrobbleEnabled: (s) => s.read(SettingsService.enableAnilistScrobble), setScrobbleEnabled: (s, v) async { - await s.setEnableAnilistScrobble(v); + await s.write(SettingsService.enableAnilistScrobble, v); await AnilistTracker.instance.setEnabled(v); }, disconnect: (a) => a.disconnectAnilist(), @@ -114,9 +114,9 @@ class TrackerConfig { displayName: t.trackers.services.simkl, isConnected: (a) => a.isSimklConnected, username: (a) => a.simklUsername, - readScrobbleEnabled: (s) => s.getEnableSimklScrobble(), + readScrobbleEnabled: (s) => s.read(SettingsService.enableSimklScrobble), setScrobbleEnabled: (s, v) async { - await s.setEnableSimklScrobble(v); + await s.write(SettingsService.enableSimklScrobble, v); await SimklTracker.instance.setEnabled(v); }, disconnect: (a) => a.disconnectSimkl(), diff --git a/lib/screens/settings/trakt_settings_screen.dart b/lib/screens/settings/trakt_settings_screen.dart index 0267c8c6..fcea43c7 100644 --- a/lib/screens/settings/trakt_settings_screen.dart +++ b/lib/screens/settings/trakt_settings_screen.dart @@ -55,8 +55,8 @@ class _TraktSettingsScreenState extends State { if (!mounted) return; setState(() { _settings = s; - _scrobbleEnabled = s.getEnableTraktScrobble(); - _watchedSyncEnabled = s.getEnableTraktWatchedSync(); + _scrobbleEnabled = s.read(SettingsService.enableTraktScrobble); + _watchedSyncEnabled = s.read(SettingsService.enableTraktWatchedSync); _loaded = true; }); } @@ -118,7 +118,7 @@ class _TraktSettingsScreenState extends State { value: _scrobbleEnabled, onChanged: (value) async { setState(() => _scrobbleEnabled = value); - await _settings!.setEnableTraktScrobble(value); + await _settings!.write(SettingsService.enableTraktScrobble, value); await TraktScrobbleService.instance.setEnabled(value); }, ), @@ -129,7 +129,7 @@ class _TraktSettingsScreenState extends State { value: _watchedSyncEnabled, onChanged: (value) async { setState(() => _watchedSyncEnabled = value); - await _settings!.setEnableTraktWatchedSync(value); + await _settings!.write(SettingsService.enableTraktWatchedSync, value); await TraktSyncService.instance.setEnabled(value); }, ), diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 6e07933f..a8ad9db6 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -602,13 +602,13 @@ class VideoPlayerScreenState extends State with WidgetsBindin try { // Load buffer size from settings final settingsService = await SettingsService.getInstance(); - _videoPlayerNavigationEnabled = settingsService.getVideoPlayerNavigationEnabled(); - _autoPipEnabled = settingsService.getAutoPip(); - _rewindOnResume = settingsService.getRewindOnResume(); - final bufferSizeMB = settingsService.getBufferSize(); - final enableHardwareDecoding = settingsService.getEnableHardwareDecoding(); - final debugLoggingEnabled = settingsService.getEnableDebugLogging(); - final useExoPlayer = settingsService.getUseExoPlayer(); + _videoPlayerNavigationEnabled = settingsService.read(SettingsService.videoPlayerNavigationEnabled); + _autoPipEnabled = settingsService.read(SettingsService.autoPip); + _rewindOnResume = settingsService.read(SettingsService.rewindOnResume); + final bufferSizeMB = settingsService.read(SettingsService.bufferSize); + final enableHardwareDecoding = settingsService.read(SettingsService.enableHardwareDecoding); + final debugLoggingEnabled = settingsService.read(SettingsService.enableDebugLogging); + final useExoPlayer = settingsService.read(SettingsService.useExoPlayer); // Initialize Windows display mode service. if (Platform.isWindows) { @@ -667,7 +667,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin await player!.configureSubtitleFonts(); await player!.setProperty('sub-ass', 'yes'); // Enable libass if (Platform.isAndroid && useExoPlayer) { - final tunneledPlayback = settingsService.getTunneledPlayback(); + final tunneledPlayback = settingsService.read(SettingsService.tunneledPlayback); await player!.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no'); } if (bufferSizeMB > 0) { @@ -716,24 +716,24 @@ class VideoPlayerScreenState extends State with WidgetsBindin await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); // Subtitle styling - await player!.setProperty('sub-font-size', settingsService.getSubtitleFontSize().toString()); - await player!.setProperty('sub-color', settingsService.getSubtitleTextColor()); - await player!.setProperty('sub-border-size', settingsService.getSubtitleBorderSize().toString()); - await player!.setProperty('sub-border-color', settingsService.getSubtitleBorderColor()); - await player!.setProperty('sub-bold', settingsService.getSubtitleBold() ? 'yes' : 'no'); - await player!.setProperty('sub-italic', settingsService.getSubtitleItalic() ? 'yes' : 'no'); - final bgOpacity = (settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt(); - final bgColor = settingsService.getSubtitleBackgroundColor().replaceFirst('#', ''); + await player!.setProperty('sub-font-size', settingsService.read(SettingsService.subtitleFontSize).toString()); + await player!.setProperty('sub-color', settingsService.read(SettingsService.subtitleTextColor)); + await player!.setProperty('sub-border-size', settingsService.read(SettingsService.subtitleBorderSize).toString()); + await player!.setProperty('sub-border-color', settingsService.read(SettingsService.subtitleBorderColor)); + await player!.setProperty('sub-bold', settingsService.read(SettingsService.subtitleBold) ? 'yes' : 'no'); + await player!.setProperty('sub-italic', settingsService.read(SettingsService.subtitleItalic) ? 'yes' : 'no'); + final bgOpacity = (settingsService.read(SettingsService.subtitleBackgroundOpacity) * 255 / 100).toInt(); + final bgColor = settingsService.read(SettingsService.subtitleBackgroundColor).replaceFirst('#', ''); await player!.setProperty( 'sub-back-color', '#${bgOpacity.toRadixString(16).padLeft(2, '0').toUpperCase()}$bgColor', ); - if (settingsService.getSubtitleBackgroundOpacity() > 0) { + if (settingsService.read(SettingsService.subtitleBackgroundOpacity) > 0) { await player!.setProperty('sub-border-style', 'background-box'); } - await player!.setProperty('sub-ass-override', settingsService.getSubAssOverride().name); + await player!.setProperty('sub-ass-override', settingsService.read(SettingsService.subAssOverride).name); await player!.setProperty('sub-ass-video-aspect-override', '1'); - await player!.setProperty('sub-pos', settingsService.getSubtitlePosition().toString()); + await player!.setProperty('sub-pos', settingsService.read(SettingsService.subtitlePosition).toString()); // Platform-specific settings if (Platform.isIOS) { @@ -742,38 +742,38 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Audio passthrough (desktop only - sends bitstream to receiver) if (PlatformDetector.isDesktopOS()) { - if (settingsService.getAudioPassthrough()) { + if (settingsService.read(SettingsService.audioPassthrough)) { await player!.setAudioPassthrough(true); } } // HDR is controlled via custom hdr-enabled property on iOS/macOS/Windows if (Platform.isIOS || Platform.isMacOS || Platform.isWindows) { - final enableHDR = settingsService.getEnableHDR(); + final enableHDR = settingsService.read(SettingsService.enableHDR); await player!.setProperty('hdr-enabled', enableHDR ? 'yes' : 'no'); } // Apply audio sync offset - final audioSyncOffset = settingsService.getAudioSyncOffset(); + final audioSyncOffset = settingsService.read(SettingsService.audioSyncOffset); if (audioSyncOffset != 0) { final offsetSeconds = audioSyncOffset / 1000.0; await player!.setProperty('audio-delay', offsetSeconds.toString()); } // Apply subtitle sync offset - final subtitleSyncOffset = settingsService.getSubtitleSyncOffset(); + final subtitleSyncOffset = settingsService.read(SettingsService.subtitleSyncOffset); if (subtitleSyncOffset != 0) { final offsetSeconds = subtitleSyncOffset / 1000.0; await player!.setProperty('sub-delay', offsetSeconds.toString()); } // Apply audio normalization (loudnorm filter) - if (settingsService.getAudioNormalization()) { + if (settingsService.read(SettingsService.audioNormalization)) { await player!.setProperty('af', 'loudnorm=I=-14:TP=-3:LRA=4'); } // Apply custom MPV config entries - final customMpvConfig = settingsService.getEnabledMpvConfigEntries(); + final customMpvConfig = SettingsService.parseMpvConfigText(settingsService.read(SettingsService.mpvConfigText)); for (final entry in customMpvConfig.entries) { try { await player!.setProperty(entry.key, entry.value); @@ -784,11 +784,11 @@ class VideoPlayerScreenState extends State with WidgetsBindin } // Set max volume limit for volume boost - final maxVolume = settingsService.getMaxVolume(); + final maxVolume = settingsService.read(SettingsService.maxVolume); await player!.setProperty('volume-max', maxVolume.toString()); // Apply saved volume (clamped to max volume) - final savedVolume = settingsService.getVolume().clamp(0.0, maxVolume.toDouble()); + final savedVolume = settingsService.read(SettingsService.volume).clamp(0.0, maxVolume.toDouble()); player!.setVolume(savedVolume); // Notify that player is ready @@ -815,7 +815,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (mounted) { try { // Check rotation lock setting before applying orientation - final isRotationLocked = settingsService.getRotationLocked(); + final isRotationLocked = settingsService.read(SettingsService.rotationLocked); if (isRotationLocked) { // Locked: Apply landscape orientation only @@ -906,7 +906,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')); // Apply frame rate matching on Android if enabled - if (Platform.isAndroid && settingsService.getMatchContentFrameRate()) { + if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) { await _applyFrameRateMatching(); } @@ -933,7 +933,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Apply frame rate matching here too, since this fallback may fire // before playbackRestart (race condition with resume positions > 0) - if (Platform.isAndroid && settingsService.getMatchContentFrameRate()) { + if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) { _applyFrameRateMatching(); } } @@ -1005,7 +1005,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin _frameRateMatchingApplied = true; final durationMs = player!.state.duration.inMilliseconds; final settingsService = await SettingsService.getInstance(); - final delaySec = settingsService.getDisplaySwitchDelay(); + final delaySec = settingsService.read(SettingsService.displaySwitchDelay); // Suppress spurious PauseEvent from MediaSession during HDMI renegotiation. // Fire Stick (and similar Android TV devices) send onPause() through the @@ -1525,7 +1525,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin final settingsService = await SettingsService.getInstance(); final preKnownFps = result.mediaInfo?.frameRate; final willAutoSwitch = - Platform.isAndroid && settingsService.getMatchContentFrameRate() && preKnownFps != null && preKnownFps > 0; + Platform.isAndroid && + settingsService.read(SettingsService.matchContentFrameRate) && + preKnownFps != null && + preKnownFps > 0; // Open video through Player if (result.videoUrl != null) { @@ -1588,15 +1591,15 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Must be called after open() since that's when ExoPlayer initializes if (player is PlayerAndroid) { await (player as PlayerAndroid).setSubtitleStyle( - fontSize: settingsService.getSubtitleFontSize().toDouble(), - textColor: settingsService.getSubtitleTextColor(), - borderSize: settingsService.getSubtitleBorderSize().toDouble(), - borderColor: settingsService.getSubtitleBorderColor(), - bgColor: settingsService.getSubtitleBackgroundColor(), - bgOpacity: settingsService.getSubtitleBackgroundOpacity(), - subtitlePosition: settingsService.getSubtitlePosition(), - bold: settingsService.getSubtitleBold(), - italic: settingsService.getSubtitleItalic(), + fontSize: settingsService.read(SettingsService.subtitleFontSize).toDouble(), + textColor: settingsService.read(SettingsService.subtitleTextColor), + borderSize: settingsService.read(SettingsService.subtitleBorderSize).toDouble(), + borderColor: settingsService.read(SettingsService.subtitleBorderColor), + bgColor: settingsService.read(SettingsService.subtitleBackgroundColor), + bgOpacity: settingsService.read(SettingsService.subtitleBackgroundOpacity), + subtitlePosition: settingsService.read(SettingsService.subtitlePosition), + bold: settingsService.read(SettingsService.subtitleBold), + italic: settingsService.read(SettingsService.subtitleItalic), ); } @@ -1714,7 +1717,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // the switch has settled. if (willAutoSwitch && mounted && player != null) { _frameRateMatchingApplied = true; - final delaySec = settingsService.getDisplaySwitchDelay(); + final delaySec = settingsService.read(SettingsService.displaySwitchDelay); final durationMs = _currentMetadata.duration ?? player!.state.duration.inMilliseconds; _suppressMediaPauseDuringFrameRateSwitch = true; Future.delayed(Duration(seconds: 2 + delaySec + 1), () { @@ -1854,8 +1857,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin player: player!, availableVersions: _availableVersions, selectedMediaIndex: widget.selectedMediaIndex, - initialBoxFitMode: settings.getDefaultBoxFitMode(), - onBoxFitModeChanged: (mode) => settings.setDefaultBoxFitMode(mode), + initialBoxFitMode: settings.read(SettingsService.defaultBoxFitMode), + onBoxFitModeChanged: (mode) => settings.write(SettingsService.defaultBoxFitMode, mode), ); _videoFilterManager!.updateVideoFilter(); @@ -1902,7 +1905,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin try { final shaderProvider = context.read(); final settings = await SettingsService.getInstance(); - final presetId = settings.getGlobalShaderPreset(); + final presetId = settings.read(SettingsService.globalShaderPreset); final preset = (shaderProvider.initialized ? shaderProvider.findPresetById(presetId) : ShaderPreset.fromId(presetId)) ?? ShaderPreset.none; @@ -1918,7 +1921,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin Future _restoreAmbientLighting() async { final shaderProvider = context.read(); final settings = await SettingsService.getInstance(); - if (!settings.getAmbientLighting()) return; + if (!settings.read(SettingsService.ambientLighting)) return; final ambientLighting = _ambientLightingService; if (ambientLighting == null || !ambientLighting.isSupported) return; @@ -2005,7 +2008,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Persist ambient lighting state final settings = await SettingsService.getInstance(); - settings.setAmbientLighting(ambientLighting.isEnabled); + settings.write(SettingsService.ambientLighting, ambientLighting.isEnabled); if (mounted) setState(() {}); } @@ -2123,7 +2126,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin receiver.onSeekForward = () async { if (player == null) return; final settings = await SettingsService.getInstance(); - final seekSeconds = settings.getSeekTimeSmall(); + final seekSeconds = settings.read(SettingsService.seekTimeSmall); if (widget.isLive && _captureBuffer != null) { await _seekLivePosition(_currentPositionEpoch + seekSeconds); return; @@ -2134,7 +2137,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin receiver.onSeekBackward = () async { if (player == null) return; final settings = await SettingsService.getInstance(); - final seekSeconds = settings.getSeekTimeSmall(); + final seekSeconds = settings.read(SettingsService.seekTimeSmall); if (widget.isLive && _captureBuffer != null) { await _seekLivePosition(_currentPositionEpoch - seekSeconds); return; @@ -2145,25 +2148,25 @@ class VideoPlayerScreenState extends State with WidgetsBindin receiver.onVolumeUp = () async { if (player == null) return; final settings = await SettingsService.getInstance(); - final maxVol = settings.getMaxVolume().toDouble(); + final maxVol = settings.read(SettingsService.maxVolume).toDouble(); final newVolume = (player!.state.volume + 10).clamp(0.0, maxVol); player!.setVolume(newVolume); - settings.setVolume(newVolume); + settings.write(SettingsService.volume, newVolume); }; receiver.onVolumeDown = () async { if (player == null) return; final settings = await SettingsService.getInstance(); - final maxVol = settings.getMaxVolume().toDouble(); + final maxVol = settings.read(SettingsService.maxVolume).toDouble(); final newVolume = (player!.state.volume - 10).clamp(0.0, maxVol); player!.setVolume(newVolume); - settings.setVolume(newVolume); + settings.write(SettingsService.volume, newVolume); }; receiver.onVolumeMute = () async { if (player == null) return; final settings = await SettingsService.getInstance(); final newVolume = player!.state.volume > 0 ? 0.0 : 100.0; player!.setVolume(newVolume); - settings.setVolume(newVolume); + settings.write(SettingsService.volume, newVolume); }; receiver.onSubtitles = _cycleSubtitleTrack; receiver.onAudioTracks = _cycleAudioTrack; @@ -2489,7 +2492,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin final isKeyboardMode = PlatformDetector.isTV() && InputModeTracker.isKeyboardMode(context); final settings = await SettingsService.getInstance(); - final autoPlayEnabled = settings.getAutoPlayNextEpisode(); + final autoPlayEnabled = settings.read(SettingsService.autoPlayNextEpisode); if (!mounted) return; setState(() { diff --git a/lib/services/base_shared_preferences_service.dart b/lib/services/base_shared_preferences_service.dart index b986e344..74439d3d 100644 --- a/lib/services/base_shared_preferences_service.dart +++ b/lib/services/base_shared_preferences_service.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; /// Base class for services that use SharedPreferences singleton pattern. /// @@ -10,30 +12,63 @@ import 'package:shared_preferences/shared_preferences.dart'; /// 3. Optionally override onInit() for post-initialization setup abstract class BaseSharedPreferencesService { static final Map _instances = {}; - late SharedPreferences _prefs; + // Single shared cache across all subclasses so writes from one service are + // visible to reads from another without per-instance cache divergence. + static Future? _cacheFuture; + + late SharedPreferencesWithCache _cache; /// Protected constructor for subclasses BaseSharedPreferencesService(); - /// Access to SharedPreferences instance - SharedPreferences get prefs => _prefs; + /// Access to the underlying preferences cache. + SharedPreferencesWithCache get prefs => _cache; - /// Initialize the SharedPreferences instance + /// Initialize the preferences instance. /// /// This method handles: /// - Singleton instance management - /// - SharedPreferences initialization + /// - One-time migration from the legacy SharedPreferences API to the + /// SharedPreferencesAsync-backed cache (idempotent across launches) /// - Calling onInit() hook for subclass-specific setup static Future initializeInstance(T Function() constructor) async { if (_instances[T] == null) { final instance = constructor(); _instances[T] = instance; - instance._prefs = await SharedPreferences.getInstance(); + instance._cache = await _ensureCache(); await instance.onInit(); } return _instances[T] as T; } + static Future _ensureCache() { + return _cacheFuture ??= () async { + final legacy = await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + legacySharedPreferencesInstance: legacy, + sharedPreferencesAsyncOptions: const SharedPreferencesOptions(), + migrationCompletedKey: 'plezy_legacy_prefs_migrated_v1', + ); + return SharedPreferencesWithCache.create(cacheOptions: const SharedPreferencesWithCacheOptions()); + }(); + } + + /// Typed read helpers — return the stored value or [defaultValue] when missing. + bool readBool(String key, {bool defaultValue = false}) => _cache.getBool(key) ?? defaultValue; + int readInt(String key, {int defaultValue = 0}) => _cache.getInt(key) ?? defaultValue; + double readDouble(String key, {double defaultValue = 0.0}) => _cache.getDouble(key) ?? defaultValue; + String readString(String key, {String defaultValue = ''}) => _cache.getString(key) ?? defaultValue; + List readStringList(String key, {List defaultValue = const []}) => + _cache.getStringList(key) ?? defaultValue; + + /// Typed write helpers — symmetric with the read helpers above; use these + /// instead of `prefs.setX(...)` so call sites stay terse. + Future writeBool(String key, bool value) => _cache.setBool(key, value); + Future writeInt(String key, int value) => _cache.setInt(key, value); + Future writeDouble(String key, double value) => _cache.setDouble(key, value); + Future writeString(String key, String value) => _cache.setString(key, value); + Future writeStringList(String key, List value) => _cache.setStringList(key, value); + /// Decode a JSON string to a Map with error handling. /// /// If [legacyStringOk] is true and the value is a plain string (not valid @@ -50,6 +85,26 @@ abstract class BaseSharedPreferencesService { } } + /// Read a value typed by [pref]; falls back to its `defaultValue`. + T read(Pref pref) => pref.readFrom(this); + + /// Write a value typed by [pref]. Pushes the post-transform value into any + /// listenable previously vended for this key so widgets rebuild automatically. + Future write(Pref pref, T value) async { + await pref.writeTo(this, value); + final n = _listenables[pref.key]; + if (n != null) (n as ValueNotifier).value = read(pref); + } + + /// Lazy per-key [ValueNotifier]. Use with [ValueListenableBuilder] to rebuild + /// when the value changes. Notifiers live for the app lifetime — do not + /// dispose them. + final Map> _listenables = {}; + + ValueNotifier listenable(Pref pref) { + return (_listenables[pref.key] ??= ValueNotifier(read(pref))) as ValueNotifier; + } + /// Hook for subclass-specific initialization after SharedPreferences is ready. /// /// Override this method to perform any setup that requires access to @@ -58,3 +113,136 @@ abstract class BaseSharedPreferencesService { // Default implementation does nothing } } + +/// Typed preference declaration. Pair with [BaseSharedPreferencesService.read] +/// and [BaseSharedPreferencesService.write] to remove per-setting `?? default` +/// boilerplate. +abstract class Pref { + final String key; + const Pref(this.key); + + /// Implementation hook — call [BaseSharedPreferencesService.read] instead. + T readFrom(BaseSharedPreferencesService svc); + + /// Implementation hook — call [BaseSharedPreferencesService.write] instead. + Future writeTo(BaseSharedPreferencesService svc, T value); +} + +class BoolPref extends Pref { + final bool defaultValue; + final void Function(bool)? onWrite; + const BoolPref(super.key, {this.defaultValue = false, this.onWrite}); + @override + bool readFrom(BaseSharedPreferencesService svc) => svc.readBool(key, defaultValue: defaultValue); + @override + Future writeTo(BaseSharedPreferencesService svc, bool value) async { + await svc.writeBool(key, value); + onWrite?.call(value); + } +} + +class IntPref extends Pref { + final int defaultValue; + final int Function(int)? transform; + const IntPref(super.key, {this.defaultValue = 0, this.transform}); + @override + int readFrom(BaseSharedPreferencesService svc) { + final raw = svc.readInt(key, defaultValue: defaultValue); + return transform == null ? raw : transform!(raw); + } + + @override + Future writeTo(BaseSharedPreferencesService svc, int value) => + svc.writeInt(key, transform == null ? value : transform!(value)); +} + +class DoublePref extends Pref { + final double defaultValue; + final double Function(double)? transform; + const DoublePref(super.key, {this.defaultValue = 0.0, this.transform}); + @override + double readFrom(BaseSharedPreferencesService svc) { + final raw = svc.readDouble(key, defaultValue: defaultValue); + return transform == null ? raw : transform!(raw); + } + + @override + Future writeTo(BaseSharedPreferencesService svc, double value) => + svc.writeDouble(key, transform == null ? value : transform!(value)); +} + +class StringPref extends Pref { + final String defaultValue; + const StringPref(super.key, {this.defaultValue = ''}); + @override + String readFrom(BaseSharedPreferencesService svc) => svc.readString(key, defaultValue: defaultValue); + @override + Future writeTo(BaseSharedPreferencesService svc, String value) => svc.writeString(key, value); +} + +/// Like [StringPref] but null = key absent. [transform] runs on write; if it +/// returns null the key is removed. +class NullableStringPref extends Pref { + final String? Function(String?)? transform; + const NullableStringPref(super.key, {this.transform}); + @override + String? readFrom(BaseSharedPreferencesService svc) => svc.prefs.getString(key); + @override + Future writeTo(BaseSharedPreferencesService svc, String? value) async { + final normalized = transform == null ? value : transform!(value); + if (normalized == null) { + await svc.prefs.remove(key); + } else { + await svc.writeString(key, normalized); + } + } +} + +class StringListPref extends Pref> { + final List defaultValue; + const StringListPref(super.key, {this.defaultValue = const []}); + @override + List readFrom(BaseSharedPreferencesService svc) => svc.readStringList(key, defaultValue: defaultValue); + @override + Future writeTo(BaseSharedPreferencesService svc, List value) => svc.writeStringList(key, value); +} + +/// Stores an enum by its [Enum.name]; falls back to [defaultValue] when the +/// stored string doesn't match any value in [values]. +class EnumPref extends Pref { + final List values; + final T defaultValue; + const EnumPref(super.key, {required this.values, required this.defaultValue}); + @override + T readFrom(BaseSharedPreferencesService svc) { + final stored = svc.prefs.getString(key); + if (stored == null) return defaultValue; + return values.firstWhere((v) => v.name == stored, orElse: () => defaultValue); + } + + @override + Future writeTo(BaseSharedPreferencesService svc, T value) => svc.writeString(key, value.name); +} + +/// Stores an arbitrary value as a JSON-encoded string. Decode failures and +/// missing keys both fall back to [defaultValue]. +class JsonPref extends Pref { + final T defaultValue; + final String Function(T) encode; + final T Function(dynamic) decode; + JsonPref(super.key, {required this.defaultValue, required this.encode, required this.decode}); + + @override + T readFrom(BaseSharedPreferencesService svc) { + final s = svc.prefs.getString(key); + if (s == null) return defaultValue; + try { + return decode(json.decode(s)); + } catch (_) { + return defaultValue; + } + } + + @override + Future writeTo(BaseSharedPreferencesService svc, T value) => svc.writeString(key, encode(value)); +} diff --git a/lib/services/discord_rpc_service.dart b/lib/services/discord_rpc_service.dart index 5937d078..d47f2bfa 100644 --- a/lib/services/discord_rpc_service.dart +++ b/lib/services/discord_rpc_service.dart @@ -76,7 +76,7 @@ class DiscordRPCService { _isInitialized = true; final settings = await SettingsService.getInstance(); - _isEnabled = settings.getEnableDiscordRPC(); + _isEnabled = settings.read(SettingsService.enableDiscordRPC); if (_isEnabled) { await _connect(); diff --git a/lib/services/display_mode_service.dart b/lib/services/display_mode_service.dart index ed0575f2..86772882 100644 --- a/lib/services/display_mode_service.dart +++ b/lib/services/display_mode_service.dart @@ -34,7 +34,7 @@ class DisplayModeService { bool anyChange = false; // Refresh rate matching. - if (_settings.getMatchRefreshRate() && fps != null && fps > 0) { + if (_settings.read(SettingsService.matchRefreshRate) && fps != null && fps > 0) { try { final success = await _matchRefreshRate(fps); anyChange |= success; @@ -44,7 +44,7 @@ class DisplayModeService { } // Dynamic range matching. - if (_settings.getMatchDynamicRange() && sigPeak != null && sigPeak > 1.0) { + if (_settings.read(SettingsService.matchDynamicRange) && sigPeak != null && sigPeak > 1.0) { try { final success = await _enableSystemHDR(); anyChange |= success; @@ -54,7 +54,7 @@ class DisplayModeService { } if (anyChange) { - final delaySec = _settings.getDisplaySwitchDelay(); + final delaySec = _settings.read(SettingsService.displaySwitchDelay); return Duration(seconds: delaySec); } diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 3cbfa0a0..49fa7764 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -125,7 +125,7 @@ class DownloadManagerService { /// pay for a second platform round-trip. static Future shouldBlockDownloadOnCellularWith(List connectivity) async { final settings = await SettingsService.getInstance(); - if (!settings.getDownloadOnWifiOnly()) return false; + if (!settings.read(SettingsService.downloadOnWifiOnly)) return false; if (connectivity.isEmpty) return false; return connectivity.contains(ConnectivityResult.mobile) && !connectivity.contains(ConnectivityResult.wifi) && @@ -610,7 +610,7 @@ class DownloadManagerService { // Get WiFi-only setting for native enforcement final settings = await SettingsService.getInstance(); - final requiresWiFi = settings.getDownloadOnWifiOnly(); + final requiresWiFi = settings.read(SettingsService.downloadOnWifiOnly); if (_storageService.isUsingSaf) { // SAF mode: use UriDownloadTask (writes directly to content:// URI, no pause/resume) diff --git a/lib/services/download_storage_service.dart b/lib/services/download_storage_service.dart index b407d992..fc97e905 100644 --- a/lib/services/download_storage_service.dart +++ b/lib/services/download_storage_service.dart @@ -47,8 +47,8 @@ class DownloadStorageService { /// Initialize with settings service (call during app startup) Future initialize(SettingsService settingsService) async { _settingsService = settingsService; - _customDownloadPath = settingsService.getCustomDownloadPath(); - _customPathType = settingsService.getCustomDownloadPathType(); + _customDownloadPath = settingsService.read(SettingsService.customDownloadPath); + _customPathType = settingsService.read(SettingsService.customDownloadPathType) ?? 'file'; // Reset cached directories to force recalculation _baseDownloadsDir = null; _artworkDirectoryPath = null; @@ -60,8 +60,8 @@ class DownloadStorageService { /// Refresh custom path from settings (call when settings change) Future refreshCustomPath() async { if (_settingsService != null) { - _customDownloadPath = _settingsService!.getCustomDownloadPath(); - _customPathType = _settingsService!.getCustomDownloadPathType(); + _customDownloadPath = _settingsService!.read(SettingsService.customDownloadPath); + _customPathType = _settingsService!.read(SettingsService.customDownloadPathType) ?? 'file'; _baseDownloadsDir = null; _artworkDirectoryPath = null; await getArtworkDirectory(); diff --git a/lib/services/external_player_service.dart b/lib/services/external_player_service.dart index 49231525..356f5bc5 100644 --- a/lib/services/external_player_service.dart +++ b/lib/services/external_player_service.dart @@ -44,7 +44,7 @@ class ExternalPlayerService { } final settings = await SettingsService.getInstance(); - final player = settings.getSelectedExternalPlayer(); + final player = settings.read(SettingsService.selectedExternalPlayer); // On Android, always use native intent to avoid url_launcher opening in browser if (Platform.isAndroid && context.mounted) { diff --git a/lib/services/keyboard_shortcuts_service.dart b/lib/services/keyboard_shortcuts_service.dart index 9c1d5130..4bee3f79 100644 --- a/lib/services/keyboard_shortcuts_service.dart +++ b/lib/services/keyboard_shortcuts_service.dart @@ -38,11 +38,11 @@ class KeyboardShortcutsService { _settingsService = await SettingsService.getInstance(); // Ensure settings service is fully initialized before loading data await Future.delayed(Duration.zero); // Allow event loop to complete - _shortcuts = _settingsService.getKeyboardShortcuts(); // Keep for legacy compatibility - _hotkeys = await _settingsService.getKeyboardHotkeys(); // Primary method - _seekTimeSmall = _settingsService.getSeekTimeSmall(); - _seekTimeLarge = _settingsService.getSeekTimeLarge(); - _maxVolume = _settingsService.getMaxVolume(); + _shortcuts = _settingsService.read(SettingsService.keyboardShortcuts); // Keep for legacy compatibility + _hotkeys = _settingsService.read(SettingsService.keyboardHotkeys); // Primary method + _seekTimeSmall = _settingsService.read(SettingsService.seekTimeSmall); + _seekTimeLarge = _settingsService.read(SettingsService.seekTimeLarge); + _maxVolume = _settingsService.read(SettingsService.maxVolume); } Map get shortcuts => Map.from(_shortcuts); @@ -59,7 +59,7 @@ class KeyboardShortcutsService { Future setShortcut(String action, String key) async { _shortcuts[action] = key; - await _settingsService.setKeyboardShortcuts(_shortcuts); + await _settingsService.write(SettingsService.keyboardShortcuts, _shortcuts); } Future setHotkey(String action, HotKey hotkey) async { @@ -67,7 +67,10 @@ class KeyboardShortcutsService { _hotkeys[action] = hotkey; // Save to persistent storage - await _settingsService.setKeyboardHotkey(action, hotkey); + await _settingsService.write(SettingsService.keyboardHotkeys, { + ..._settingsService.read(SettingsService.keyboardHotkeys), + action: hotkey, + }); // Verify local cache is still correct if (_hotkeys[action] != hotkey) { @@ -76,16 +79,16 @@ class KeyboardShortcutsService { } Future refreshFromStorage() async { - _hotkeys = await _settingsService.getKeyboardHotkeys(); - _seekTimeSmall = _settingsService.getSeekTimeSmall(); - _seekTimeLarge = _settingsService.getSeekTimeLarge(); + _hotkeys = _settingsService.read(SettingsService.keyboardHotkeys); + _seekTimeSmall = _settingsService.read(SettingsService.seekTimeSmall); + _seekTimeLarge = _settingsService.read(SettingsService.seekTimeLarge); } Future resetToDefaults() async { - _shortcuts = _settingsService.getDefaultKeyboardShortcuts(); - _hotkeys = _settingsService.getDefaultKeyboardHotkeys(); - await _settingsService.setKeyboardShortcuts(_shortcuts); - await _settingsService.setKeyboardHotkeys(_hotkeys); + _shortcuts = SettingsService.defaultKeyboardShortcuts(); + _hotkeys = SettingsService.defaultKeyboardHotkeys(); + await _settingsService.write(SettingsService.keyboardShortcuts, _shortcuts); + await _settingsService.write(SettingsService.keyboardHotkeys, _hotkeys); // Refresh cache to ensure consistency await refreshFromStorage(); } @@ -265,12 +268,12 @@ class KeyboardShortcutsService { case 'volume_up': final newVolume = (player.state.volume + 10).clamp(0.0, _maxVolume.toDouble()); player.setVolume(newVolume); - _settingsService.setVolume(newVolume); + _settingsService.write(SettingsService.volume, newVolume); break; case 'volume_down': final newVolume = (player.state.volume - 10).clamp(0.0, _maxVolume.toDouble()); player.setVolume(newVolume); - _settingsService.setVolume(newVolume); + _settingsService.write(SettingsService.volume, newVolume); break; case 'seek_forward': performSeek(_seekTimeSmall); @@ -290,7 +293,7 @@ class KeyboardShortcutsService { case 'mute_toggle': final newVolume = player.state.volume > 0 ? 0.0 : 100.0; player.setVolume(newVolume); - _settingsService.setVolume(newVolume); + _settingsService.write(SettingsService.volume, newVolume); break; case 'subtitle_toggle': onToggleSubtitles?.call(); @@ -316,16 +319,16 @@ class KeyboardShortcutsService { case 'speed_increase': final newRateUp = (player.state.rate + 0.25).clamp(0.25, 3.0); player.setRate(newRateUp); - _settingsService.setDefaultPlaybackSpeed(newRateUp); + _settingsService.write(SettingsService.defaultPlaybackSpeed, newRateUp); break; case 'speed_decrease': final newRateDown = (player.state.rate - 0.25).clamp(0.25, 3.0); player.setRate(newRateDown); - _settingsService.setDefaultPlaybackSpeed(newRateDown); + _settingsService.write(SettingsService.defaultPlaybackSpeed, newRateDown); break; case 'speed_reset': player.setRate(1.0); - _settingsService.setDefaultPlaybackSpeed(1.0); + _settingsService.write(SettingsService.defaultPlaybackSpeed, 1.0); break; case 'sub_seek_next': player.command(['sub-seek', '1']); diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index 19407a70..f3737126 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -242,7 +242,7 @@ class MultiServerManager { .fetchServerPrefs() .then((_) { final threshold = client.watchedThresholdPercent; - SettingsService.instanceOrNull?.setWatchedThreshold(serverId, threshold); + SettingsService.instanceOrNull?.write(SettingsService.watchedThresholdPref(serverId), threshold); }) .catchError((Object e, StackTrace st) { appLogger.w('fetchServerPrefs failed for ${server.name}', error: e, stackTrace: st); diff --git a/lib/services/offline_watch_sync_service.dart b/lib/services/offline_watch_sync_service.dart index 5b1a1ec2..3a453fe9 100644 --- a/lib/services/offline_watch_sync_service.dart +++ b/lib/services/offline_watch_sync_service.dart @@ -44,7 +44,7 @@ class OfflineWatchSyncService extends ChangeNotifier { if (client != null && client.serverPrefs.isNotEmpty) { return client.watchedThresholdPercent / 100.0; } - final cached = SettingsService.instanceOrNull?.getWatchedThreshold(serverId) ?? 90; + final cached = SettingsService.instanceOrNull?.read(SettingsService.watchedThresholdPref(serverId)) ?? 90; return cached / 100.0; } diff --git a/lib/services/settings_export_service.dart b/lib/services/settings_export_service.dart index 6d03da32..63c63b03 100644 --- a/lib/services/settings_export_service.dart +++ b/lib/services/settings_export_service.dart @@ -11,6 +11,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../utils/app_logger.dart'; import '../utils/platform_detector.dart'; import 'file_picker_service.dart'; +import 'settings_service.dart'; import 'storage_service.dart'; class ImportResult { @@ -101,7 +102,7 @@ class SettingsExportService { /// prefix stripped so they can be re-scoped on import. Keys belonging to any /// OTHER user are skipped (we only export the active user's prefs). static Map buildExportMap( - SharedPreferences prefs, { + SharedPreferencesWithCache prefs, { String? currentUserUuid, String appVersion = '', }) { @@ -110,7 +111,7 @@ class SettingsExportService { ? '$_userPrefixRoot${currentUserUuid}_' : null; - for (final fullKey in prefs.getKeys()) { + for (final fullKey in prefs.keys) { String baseKey; if (currentUserPrefix != null && fullKey.startsWith(currentUserPrefix)) { baseKey = fullKey.substring(currentUserPrefix.length); @@ -159,7 +160,7 @@ class SettingsExportService { /// Throws [SettingsExportException] for structural problems. static Future applyImportMap( Map data, - SharedPreferences prefs, { + SharedPreferencesWithCache prefs, { required String currentUserUuid, }) async { final version = data['formatVersion']; @@ -222,7 +223,7 @@ class SettingsExportService { return prefixes.any(baseKey.startsWith); } - static Future _writeTyped(SharedPreferences prefs, String key, String type, Object? value) async { + static Future _writeTyped(SharedPreferencesWithCache prefs, String key, String type, Object? value) async { try { switch (type) { case _typeBool: @@ -270,7 +271,7 @@ class SettingsExportService { /// /// Throws [SettingsExportException] on failure. static Future exportToFile() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = (await SettingsService.getInstance()).prefs; final storage = await StorageService.getInstance(); String appVersion = ''; try { @@ -359,7 +360,7 @@ class SettingsExportService { throw const InvalidExportFileException('Invalid export file'); } - final prefs = await SharedPreferences.getInstance(); + final prefs = (await SettingsService.getInstance()).prefs; return applyImportMap(data, prefs, currentUserUuid: uuid); } } diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 1b80c9dd..24a44153 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -9,6 +9,8 @@ import '../i18n/strings.g.dart'; import '../models/mpv_config_models.dart'; import '../models/external_player_models.dart'; import 'base_shared_preferences_service.dart'; +export 'base_shared_preferences_service.dart' + show Pref, BoolPref, IntPref, DoublePref, StringPref, NullableStringPref, StringListPref, EnumPref, JsonPref; import '../utils/platform_detector.dart'; import 'trackers/tracker_constants.dart'; @@ -31,108 +33,385 @@ enum EpisodePosterMode { seriesPoster, seasonPoster, episodeThumbnail } enum SubAssOverride { no, yes, scale, force, strip } -class SettingsService extends BaseSharedPreferencesService { - static const String _keyThemeMode = 'theme_mode'; - static const String _keyEnableDebugLogging = 'enable_debug_logging'; - static const String _keyCrashReporting = 'crash_reporting'; - static const String _keyBufferSize = 'buffer_size'; - static const String _keyBufferSizeMigratedToAuto = 'buffer_size_migrated_to_auto'; - static const String _keyKeyboardShortcuts = 'keyboard_shortcuts'; - static const String _keyKeyboardHotkeys = 'keyboard_hotkeys'; - static const String _keyEnableHardwareDecoding = 'enable_hardware_decoding'; - static const String _keyEnableHDR = 'enable_hdr'; - static const String _keyPreferredVideoCodec = 'preferred_video_codec'; - static const String _keyPreferredAudioCodec = 'preferred_audio_codec'; - static const String _keyLibraryDensity = 'library_density'; - static const String _keyViewMode = 'view_mode'; - static const String _keyUseSeasonPoster = 'use_season_poster'; // Legacy key for migration - static const String _keyEpisodePosterMode = 'episode_poster_mode'; - static const String _keySeekTimeSmall = 'seek_time_small'; - static const String _keySeekTimeLarge = 'seek_time_large'; - static const String _keyRewindOnResume = 'rewind_on_resume'; - static const String _keyMediaVersionPreferences = 'media_version_preferences'; - static const String _keyShowHeroSection = 'show_hero_section'; - static const String _keyUseGlobalHubs = 'use_global_hubs'; - static const String _keyShowServerNameOnHubs = 'show_server_name_on_hubs'; - static const String _keyGroupLibrariesByServer = 'group_libraries_by_server'; - static const String _keySleepTimerDuration = 'sleep_timer_duration'; - static const String _keyAudioSyncOffset = 'audio_sync_offset'; - static const String _keySubtitleSyncOffset = 'subtitle_sync_offset'; - static const String _keyVolume = 'volume'; - static const String _keyRotationLocked = 'rotation_locked'; - static const String _keySubtitleFontSize = 'subtitle_font_size'; - static const String _keySubtitleTextColor = 'subtitle_text_color'; - static const String _keySubtitleBorderSize = 'subtitle_border_size'; - static const String _keySubtitleBorderColor = 'subtitle_border_color'; - static const String _keySubtitleBackgroundColor = 'subtitle_background_color'; - static const String _keySubtitleBackgroundOpacity = 'subtitle_background_opacity'; - static const String _keySubtitlePosition = 'subtitle_position'; - static const String _keySubAssOverride = 'sub_ass_override'; - static const String _keySubtitleBold = 'subtitle_bold'; - static const String _keySubtitleItalic = 'subtitle_italic'; - static const String _keyAppLocale = 'app_locale'; - static const String _keyRememberTrackSelections = 'remember_track_selections'; - static const String _keyClickVideoTogglesPlayback = 'click_video_toggles_playback'; - static const String _keyAutoSkipIntro = 'auto_skip_intro'; - static const String _keyAutoSkipCredits = 'auto_skip_credits'; - static const String _keyAutoSkipDelay = 'auto_skip_delay'; - static const String _keyIntroPattern = 'intro_pattern'; - static const String _keyCreditsPattern = 'credits_pattern'; +const String _bufferSizeMigratedKey = 'buffer_size_migrated_to_auto'; +const String _legacyUseSeasonPosterKey = 'use_season_poster'; +const String _legacyMpvConfigEntriesKey = 'mpv_config_entries'; +/// One-time auto-reset migration for buffer size. +class _BufferSizePref extends IntPref { + const _BufferSizePref() : super('buffer_size'); + + @override + int readFrom(BaseSharedPreferencesService svc) { + // SharedPreferences updates in-memory cache synchronously, so the + // unawaited disk-flush futures are safe here (idempotent if re-run). + if (svc.prefs.getBool(_bufferSizeMigratedKey) != true) { + svc.prefs.remove(key); + svc.prefs.setBool(_bufferSizeMigratedKey, true); + } + return super.readFrom(svc); + } +} + +/// Migrates from the legacy enum-string format and clamps to 1..5. +class _LibraryDensityPref extends Pref { + const _LibraryDensityPref() : super('library_density'); + + @override + int readFrom(BaseSharedPreferencesService svc) { + try { + final intVal = svc.prefs.getInt(key); + if (intVal != null) return intVal.clamp(LibraryDensity.min, LibraryDensity.max); + } on TypeError { + // Stored value is a String from old enum format — fall through to migration. + } + String? strVal; + try { + strVal = svc.prefs.getString(key); + } on TypeError { + // Value exists but isn't a String either. + } + final result = switch (strVal) { + 'compact' => 2, + 'comfortable' => 4, + _ => LibraryDensity.defaultValue, + }; + svc.prefs.setInt(key, result); + return result; + } + + @override + Future writeTo(BaseSharedPreferencesService svc, int value) => + svc.writeInt(key, value.clamp(LibraryDensity.min, LibraryDensity.max)); +} + +/// Migrates from the legacy `use_season_poster` boolean key. +class _EpisodePosterModePref extends EnumPref { + const _EpisodePosterModePref() + : super('episode_poster_mode', values: EpisodePosterMode.values, defaultValue: EpisodePosterMode.episodeThumbnail); + + @override + EpisodePosterMode readFrom(BaseSharedPreferencesService svc) { + final legacyValue = svc.prefs.getBool(_legacyUseSeasonPosterKey); + if (legacyValue != null) { + final migrated = legacyValue ? EpisodePosterMode.seasonPoster : EpisodePosterMode.seriesPoster; + svc.prefs.remove(_legacyUseSeasonPosterKey); + svc.prefs.setString(key, migrated.name); + return migrated; + } + return super.readFrom(svc); + } +} + +/// Stored as the language code; null/empty falls back to the device locale. +class _AppLocalePref extends Pref { + const _AppLocalePref() : super('app_locale'); + + @override + AppLocale readFrom(BaseSharedPreferencesService svc) { + final code = svc.prefs.getString(key); + if (code == null || code.isEmpty) return AppLocaleUtils.findDeviceLocale(); + return AppLocale.values.asNameMap()[code] ?? AppLocale.en; + } + + @override + Future writeTo(BaseSharedPreferencesService svc, AppLocale value) => svc.writeString(key, value.languageCode); +} + +/// Mobile-only with a macOS-disabled-by-default rule; forced off on TV and non-mobile platforms. +class _AutoPipPref extends Pref { + const _AutoPipPref() : super('auto_pip'); + + @override + bool readFrom(BaseSharedPreferencesService svc) { + if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) return false; + if (PlatformDetector.isTV()) return false; + return svc.prefs.getBool(key) ?? !Platform.isMacOS; + } + + @override + Future writeTo(BaseSharedPreferencesService svc, bool value) => svc.writeBool(key, value); +} + +String? _trimEmptyAsNull(String? v) { + final t = v?.trim(); + return (t == null || t.isEmpty) ? null : t; +} + +String _legacyMpvEntriesToText(List entries) { + final lines = []; + for (final item in entries) { + if (item is Map) { + final k = item['key'] as String? ?? ''; + final v = item['value'] as String? ?? ''; + final enabled = item['isEnabled'] as bool? ?? true; + if (k.isNotEmpty) { + lines.add(enabled ? '$k=$v' : '#$k=$v'); + } + } + } + return lines.join('\n'); +} + +class _MpvConfigTextPref extends StringPref { + const _MpvConfigTextPref() : super('mpv_config_text'); + + @override + String readFrom(BaseSharedPreferencesService svc) { + final text = svc.prefs.getString(key); + if (text != null) return text; + + final legacyJson = svc.prefs.getString(_legacyMpvConfigEntriesKey); + if (legacyJson == null) return ''; + + try { + final migrated = _legacyMpvEntriesToText(json.decode(legacyJson) as List); + svc.prefs.setString(key, migrated); + return migrated; + } catch (e, st) { + appLogger.w('SettingsService: failed to migrate mpv config', error: e, stackTrace: st); + return ''; + } + } +} + +List _decodeMpvPresets(dynamic raw) { + return (raw as List).map((e) { + final map = e as Map; + if (map.containsKey('entries') && !map.containsKey('text')) { + map['text'] = _legacyMpvEntriesToText(map['entries'] as List); + } + return MpvPreset.fromJson(map); + }).toList(); +} + +Map _defaultKeyboardShortcuts() => { + 'play_pause': 'Space', + 'volume_up': 'Arrow Up', + 'volume_down': 'Arrow Down', + 'seek_forward': 'Arrow Right', + 'seek_backward': 'Arrow Left', + 'seek_forward_large': 'Shift+Arrow Right', + 'seek_backward_large': 'Shift+Arrow Left', + 'fullscreen_toggle': 'F', + 'mute_toggle': 'M', + 'subtitle_toggle': 'S', + 'audio_track_next': 'A', + 'subtitle_track_next': 'Shift+S', + 'chapter_next': 'N', + 'chapter_previous': 'P', + 'speed_increase': 'Plus', + 'speed_decrease': 'Minus', + 'speed_reset': 'R', + 'sub_seek_next': 'Ctrl+Right', + 'sub_seek_prev': 'Ctrl+Left', +}; + +Map _defaultKeyboardHotkeys() => { + 'play_pause': const HotKey(key: PhysicalKeyboardKey.space), + 'volume_up': const HotKey(key: PhysicalKeyboardKey.arrowUp), + 'volume_down': const HotKey(key: PhysicalKeyboardKey.arrowDown), + 'seek_forward': const HotKey(key: PhysicalKeyboardKey.arrowRight), + 'seek_backward': const HotKey(key: PhysicalKeyboardKey.arrowLeft), + 'seek_forward_large': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]), + 'seek_backward_large': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]), + 'fullscreen_toggle': const HotKey(key: PhysicalKeyboardKey.keyF), + 'mute_toggle': const HotKey(key: PhysicalKeyboardKey.keyM), + 'subtitle_toggle': const HotKey(key: PhysicalKeyboardKey.keyS), + 'audio_track_next': const HotKey(key: PhysicalKeyboardKey.keyA), + 'subtitle_track_next': const HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]), + 'chapter_next': const HotKey(key: PhysicalKeyboardKey.keyN), + 'chapter_previous': const HotKey(key: PhysicalKeyboardKey.keyP), + 'episode_next': const HotKey(key: PhysicalKeyboardKey.keyN, modifiers: [HotKeyModifier.shift]), + 'episode_previous': const HotKey(key: PhysicalKeyboardKey.keyP, modifiers: [HotKeyModifier.shift]), + 'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal), + 'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus), + 'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR), + 'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]), + 'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]), + 'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG), + 'skip_marker': const HotKey(key: PhysicalKeyboardKey.enter), +}; + +Map _decodeKeyboardShortcuts(dynamic raw) { + final stored = (raw as Map).map((k, v) => MapEntry(k, v.toString())); + // Merge with defaults so newly-added defaults appear without resetting customizations. + return {..._defaultKeyboardShortcuts(), ...stored}; +} + +Map _decodeKeyboardHotkeys(dynamic raw) { + final result = {}; + for (final entry in (raw as Map).entries) { + final hk = SettingsService.deserializeHotKey(entry.value as Map); + if (hk != null) result[entry.key] = hk; + } + return {..._defaultKeyboardHotkeys(), ...result}; +} + +class SettingsService extends BaseSharedPreferencesService { static const String defaultIntroPattern = r'(?:^|\b)(?:intro(?:duction)?|opening)(?:\b|$)|^op(?:\s?\d+)?$'; static const String defaultCreditsPattern = r'(?:^|\b)(?:outro|closing|credits?|ending)(?:\b|$)|^ed(?:\s?\d+)?$'; - static const String _keyCustomDownloadPath = 'custom_download_path'; - static const String _keyCustomDownloadPathType = 'custom_download_path_type'; - static const String _keyDownloadOnWifiOnly = 'download_on_wifi_only'; - static const String _keyAutoRemoveWatchedDownloads = 'auto_remove_watched_downloads'; - static const String _keyAutoCheckUpdatesOnStartup = 'auto_check_updates_on_startup'; - static const String _prefixWatchedThreshold = 'watched_threshold_'; - static const String _keyVideoPlayerNavigationEnabled = 'video_player_navigation_enabled'; - static const String _keyShowPerformanceOverlay = 'show_performance_overlay'; - static const String _keyAutoHidePerformanceOverlay = 'auto_hide_performance_overlay'; - static const String _keyMpvConfigEntries = 'mpv_config_entries'; // Legacy - static const String _keyMpvConfigText = 'mpv_config_text'; - static const String _keyMpvConfigPresets = 'mpv_config_presets'; - static const String _keyMaxVolume = 'max_volume'; - static const String _keyEnableDiscordRPC = 'enable_discord_rpc'; - static const String _keyEnableTraktScrobble = 'enable_trakt_scrobble'; - static const String _keyEnableTraktWatchedSync = 'enable_trakt_watched_sync'; - static const String _keyEnableMalScrobble = 'enable_mal_scrobble'; - static const String _keyEnableAnilistScrobble = 'enable_anilist_scrobble'; - static const String _keyEnableSimklScrobble = 'enable_simkl_scrobble'; - static const String _prefixTrackerLibraryFilterMode = 'tracker_library_filter_mode_'; - static const String _prefixTrackerLibraryFilterIds = 'tracker_library_filter_ids_'; - static const String _keyEnableCompanionRemoteServer = 'enable_companion_remote_server'; - static const String _keyAutoPip = 'auto_pip'; - static const String _keyMatchContentFrameRate = 'match_content_frame_rate'; - static const String _keyTunneledPlayback = 'tunneled_playback'; - static const String _keyDefaultPlaybackSpeed = 'default_playback_speed'; - static const String _keyDefaultBoxFitMode = 'default_box_fit_mode'; - static const String _keyDefaultQualityPreset = 'default_quality_preset'; - static const String _keyAutoPlayNextEpisode = 'auto_play_next_episode'; - static const String _keyUseExoPlayer = 'use_exoplayer'; - static const String _keyAlwaysKeepSidebarOpen = 'always_keep_sidebar_open'; - static const String _keyShowUnwatchedCount = 'show_unwatched_count'; - static const String _keyShowEpisodeNumberOnCards = 'show_episode_number_on_cards'; - static const String _keyHideSpoilers = 'hide_spoilers'; - static const String _keyShowNavBarLabels = 'show_nav_bar_labels'; - static const String _keyGlobalShaderPreset = 'global_shader_preset'; - static const String _keyRequireProfileSelectionOnOpen = 'require_profile_selection_on_open'; - static const String _keyUseExternalPlayer = 'use_external_player'; - static const String _keySelectedExternalPlayer = 'selected_external_player'; - static const String _keyCustomExternalPlayers = 'custom_external_players'; - static const String _keyConfirmExitOnBack = 'confirm_exit_on_back'; - static const String _keyForceTvMode = 'force_tv_mode'; - static const String _keyAmbientLighting = 'ambient_lighting'; - static const String _keyAudioPassthrough = 'audio_passthrough'; - static const String _keyAudioNormalization = 'audio_normalization'; - static const String _keyCustomShaderPresets = 'custom_shader_presets'; - static const String _keyLiveTvDefaultFavorites = 'live_tv_default_favorites'; - static const String _keyCustomRelayUrl = 'custom_relay_url'; - static const String _keyRecentRooms = 'watch_together_recent_rooms'; - static const String _keyMatchRefreshRate = 'match_refresh_rate'; - static const String _keyMatchDynamicRange = 'match_dynamic_range'; - static const String _keyDisplaySwitchDelay = 'display_switch_delay'; + + static const enableDebugLogging = BoolPref('enable_debug_logging', onWrite: setLoggerLevel); + static const crashReporting = BoolPref('crash_reporting', defaultValue: true); + static const enableHardwareDecoding = BoolPref('enable_hardware_decoding', defaultValue: true); + static const enableHDR = BoolPref('enable_hdr', defaultValue: true); + static const preferredVideoCodec = StringPref('preferred_video_codec', defaultValue: 'auto'); + static const preferredAudioCodec = StringPref('preferred_audio_codec', defaultValue: 'auto'); + static const viewMode = EnumPref('view_mode', values: ViewMode.values, defaultValue: ViewMode.grid); + static const seekTimeSmall = IntPref('seek_time_small', defaultValue: 10); + static const seekTimeLarge = IntPref('seek_time_large', defaultValue: 30); + static const rewindOnResume = IntPref('rewind_on_resume'); + static const showHeroSection = BoolPref('show_hero_section', defaultValue: true); + static const useGlobalHubs = BoolPref('use_global_hubs', defaultValue: true); + static const showServerNameOnHubs = BoolPref('show_server_name_on_hubs'); + static const groupLibrariesByServer = BoolPref('group_libraries_by_server', defaultValue: true); + static const sleepTimerDuration = IntPref('sleep_timer_duration', defaultValue: 30); + static const audioSyncOffset = IntPref('audio_sync_offset'); + static const subtitleSyncOffset = IntPref('subtitle_sync_offset'); + static const volume = DoublePref('volume', defaultValue: 100.0); + static const rotationLocked = BoolPref('rotation_locked', defaultValue: true); + static const subtitleFontSize = IntPref('subtitle_font_size', defaultValue: 38); + static const subtitleTextColor = StringPref('subtitle_text_color', defaultValue: '#FFFFFF'); + static const subtitleBorderSize = IntPref('subtitle_border_size', defaultValue: 3); + static const subtitleBorderColor = StringPref('subtitle_border_color', defaultValue: '#000000'); + static const subtitleBackgroundColor = StringPref('subtitle_background_color', defaultValue: '#000000'); + static const subtitleBackgroundOpacity = IntPref('subtitle_background_opacity'); + static const subAssOverride = EnumPref( + 'sub_ass_override', + values: SubAssOverride.values, + defaultValue: SubAssOverride.no, + ); + static const subtitleBold = BoolPref('subtitle_bold'); + static const subtitleItalic = BoolPref('subtitle_italic'); + static const rememberTrackSelections = BoolPref('remember_track_selections', defaultValue: true); + static const clickVideoTogglesPlayback = BoolPref('click_video_toggles_playback'); + static const autoSkipIntro = BoolPref('auto_skip_intro'); + static const autoSkipCredits = BoolPref('auto_skip_credits'); + static const autoSkipDelay = IntPref('auto_skip_delay', defaultValue: 5); + static const introPattern = StringPref('intro_pattern', defaultValue: defaultIntroPattern); + static const creditsPattern = StringPref('credits_pattern', defaultValue: defaultCreditsPattern); + static const customDownloadPathType = NullableStringPref('custom_download_path_type'); + static const downloadOnWifiOnly = BoolPref('download_on_wifi_only'); + static const autoRemoveWatchedDownloads = BoolPref('auto_remove_watched_downloads'); + static const autoCheckUpdatesOnStartup = BoolPref('auto_check_updates_on_startup', defaultValue: true); + static const showPerformanceOverlay = BoolPref('show_performance_overlay'); + static const autoHidePerformanceOverlay = BoolPref('auto_hide_performance_overlay', defaultValue: true); + static const enableDiscordRPC = BoolPref('enable_discord_rpc'); + static const enableTraktScrobble = BoolPref('enable_trakt_scrobble', defaultValue: true); + static const enableTraktWatchedSync = BoolPref('enable_trakt_watched_sync', defaultValue: true); + static const enableMalScrobble = BoolPref('enable_mal_scrobble', defaultValue: true); + static const enableAnilistScrobble = BoolPref('enable_anilist_scrobble', defaultValue: true); + static const enableSimklScrobble = BoolPref('enable_simkl_scrobble', defaultValue: true); + static const matchContentFrameRate = BoolPref('match_content_frame_rate'); + static const tunneledPlayback = BoolPref('tunneled_playback', defaultValue: true); + static const defaultQualityPreset = StringPref('default_quality_preset', defaultValue: 'original'); + static const autoPlayNextEpisode = BoolPref('auto_play_next_episode', defaultValue: true); + static const useExoPlayer = BoolPref('use_exoplayer', defaultValue: true); + static const alwaysKeepSidebarOpen = BoolPref('always_keep_sidebar_open'); + static const showUnwatchedCount = BoolPref('show_unwatched_count', defaultValue: true); + static const showEpisodeNumberOnCards = BoolPref('show_episode_number_on_cards', defaultValue: true); + static const hideSpoilers = BoolPref('hide_spoilers'); + static const showNavBarLabels = BoolPref('show_nav_bar_labels', defaultValue: true); + static const globalShaderPreset = StringPref('global_shader_preset', defaultValue: 'none'); + static const requireProfileSelectionOnOpen = BoolPref('require_profile_selection_on_open'); + static const useExternalPlayer = BoolPref('use_external_player'); + static const confirmExitOnBack = BoolPref('confirm_exit_on_back', defaultValue: true); + static const forceTvMode = BoolPref('force_tv_mode'); + static const ambientLighting = BoolPref('ambient_lighting'); + static const audioPassthrough = BoolPref('audio_passthrough'); + static const audioNormalization = BoolPref('audio_normalization'); + static const liveTvDefaultFavorites = BoolPref('live_tv_default_favorites'); + static const matchRefreshRate = BoolPref('match_refresh_rate'); + static const matchDynamicRange = BoolPref('match_dynamic_range'); + static const appLocale = _AppLocalePref(); + static const autoPip = _AutoPipPref(); + static const customDownloadPath = NullableStringPref('custom_download_path'); + static final customRelayUrl = NullableStringPref('custom_relay_url', transform: _trimEmptyAsNull); + static const recentRooms = NullableStringPref('watch_together_recent_rooms'); + + static final maxVolume = IntPref('max_volume', defaultValue: 100, transform: (v) => v.clamp(100, 300)); + static final subtitlePosition = IntPref('subtitle_position', defaultValue: 100, transform: (v) => v.clamp(0, 100)); + static final defaultPlaybackSpeed = DoublePref( + 'default_playback_speed', + defaultValue: 1.0, + transform: (v) => v.clamp(0.5, 3.0), + ); + static final defaultBoxFitMode = IntPref('default_box_fit_mode', transform: (v) => v.clamp(0, 2)); + static final displaySwitchDelay = IntPref('display_switch_delay', transform: (v) => v.clamp(0, 10)); + + static final themeMode = EnumPref( + 'theme_mode', + values: ThemeMode.values, + defaultValue: TvDetectionService.isTVSync() ? ThemeMode.oled : ThemeMode.system, + ); + static final videoPlayerNavigationEnabled = BoolPref( + 'video_player_navigation_enabled', + defaultValue: TvDetectionService.isTVSync(), + ); + static final enableCompanionRemoteServer = BoolPref( + 'enable_companion_remote_server', + defaultValue: PlatformDetector.isDesktopOS(), + ); + + static const bufferSize = _BufferSizePref(); + static const libraryDensity = _LibraryDensityPref(); + static const episodePosterMode = _EpisodePosterModePref(); + static const mpvConfigText = _MpvConfigTextPref(); + + static final keyboardShortcuts = JsonPref>( + 'keyboard_shortcuts', + defaultValue: _defaultKeyboardShortcuts(), + encode: json.encode, + decode: _decodeKeyboardShortcuts, + ); + static final keyboardHotkeys = JsonPref>( + 'keyboard_hotkeys', + defaultValue: _defaultKeyboardHotkeys(), + encode: (v) => json.encode(v.map((k, hk) => MapEntry(k, SettingsService.serializeHotKey(hk)))), + decode: _decodeKeyboardHotkeys, + ); + static final mediaVersionPreferences = JsonPref>( + 'media_version_preferences', + defaultValue: const {}, + encode: json.encode, + decode: (raw) => (raw as Map).map((k, v) => MapEntry(k, v as int)), + ); + static final customShaderPresets = JsonPref>>( + 'custom_shader_presets', + defaultValue: const [], + encode: json.encode, + decode: (raw) => (raw as List).cast>(), + ); + static final selectedExternalPlayer = JsonPref( + 'selected_external_player', + defaultValue: KnownPlayers.systemDefault, + encode: (p) => json.encode(p.toJson()), + decode: (raw) => ExternalPlayer.fromJson(raw as Map), + ); + static final customExternalPlayers = JsonPref>( + 'custom_external_players', + defaultValue: const [], + encode: (v) => json.encode(v.map((p) => p.toJson()).toList()), + decode: (raw) => (raw as List).map((e) => ExternalPlayer.fromJson(e as Map)).toList(), + ); + static final mpvPresets = JsonPref>( + 'mpv_config_presets', + defaultValue: const [], + encode: (v) => json.encode(v.map((p) => p.toJson()).toList()), + decode: _decodeMpvPresets, + ); + + static IntPref watchedThresholdPref(String serverId) => IntPref('watched_threshold_$serverId', defaultValue: 90); + + static EnumPref trackerFilterModePref(TrackerService s) => EnumPref( + 'tracker_library_filter_mode_${s.name}', + values: TrackerLibraryFilterMode.values, + defaultValue: TrackerLibraryFilterMode.blacklist, + ); + + static StringListPref trackerFilterIdsPref(TrackerService s) => + StringListPref('tracker_library_filter_ids_${s.name}'); SettingsService._(); @@ -146,550 +425,59 @@ class SettingsService extends BaseSharedPreferencesService { /// Synchronous access to the singleton, or null if not yet initialized. static SettingsService? get instanceOrNull => _cachedInstance; - /// Generic helper to get an enum value from preferences - T _getEnumValue(String key, List values, T defaultValue) { - final stored = prefs.getString(key); - if (stored == null) return defaultValue; - return values.firstWhere((v) => v.name == stored, orElse: () => defaultValue); + static Map defaultKeyboardShortcuts() => _defaultKeyboardShortcuts(); + static Map defaultKeyboardHotkeys() => _defaultKeyboardHotkeys(); + + /// Null keys bypass the filter so we err on the side of syncing. + bool isLibraryAllowedForTracker(TrackerService service, String? libraryGlobalKey) { + if (libraryGlobalKey == null) return true; + final inList = read(trackerFilterIdsPref(service)).contains(libraryGlobalKey); + final mode = read(trackerFilterModePref(service)); + return mode == TrackerLibraryFilterMode.blacklist ? !inList : inList; } - // Theme Mode - Future setThemeMode(ThemeMode mode) async { - await prefs.setString(_keyThemeMode, mode.name); - } - - ThemeMode getThemeMode() { - final stored = prefs.getString(_keyThemeMode); - if (stored == null) { - // Default to OLED on Android TV, system elsewhere - return TvDetectionService.isTVSync() ? ThemeMode.oled : ThemeMode.system; + Future removeCustomExternalPlayer(String id) async { + final players = read(customExternalPlayers).where((p) => p.id != id).toList(); + await write(customExternalPlayers, players); + if (read(selectedExternalPlayer).id == id) { + await write(selectedExternalPlayer, KnownPlayers.systemDefault); } - return _getEnumValue(_keyThemeMode, ThemeMode.values, ThemeMode.system); } - // Debug Logging - Future setEnableDebugLogging(bool enabled) async { - await prefs.setBool(_keyEnableDebugLogging, enabled); - // Update logger level immediately when setting changes - setLoggerLevel(enabled); - } - - bool getEnableDebugLogging() { - return prefs.getBool(_keyEnableDebugLogging) ?? false; - } - - // Crash Reporting - Future setCrashReporting(bool enabled) async { - await prefs.setBool(_keyCrashReporting, enabled); - } - - bool getCrashReporting() { - return prefs.getBool(_keyCrashReporting) ?? true; // Default enabled - } - - // Default video quality preset (persisted as enum name) - Future setDefaultQualityPreset(String presetName) async { - await prefs.setString(_keyDefaultQualityPreset, presetName); - } - - String getDefaultQualityPreset() { - return prefs.getString(_keyDefaultQualityPreset) ?? 'original'; - } - - // Buffer Size (in MB) - Future setBufferSize(int sizeInMB) async { - await prefs.setInt(_keyBufferSize, sizeInMB); - } - - int getBufferSize() { - // One-time migration: reset existing users to Auto. - // SharedPreferences updates in-memory cache synchronously, so the - // unawaited disk-flush futures are safe here (idempotent if re-run). - if (prefs.getBool(_keyBufferSizeMigratedToAuto) != true) { - prefs.remove(_keyBufferSize); - prefs.setBool(_keyBufferSizeMigratedToAuto, true); + /// Parse raw config text into a `Map` (skip blanks and # comments). + static Map parseMpvConfigText(String text) { + final result = {}; + for (final line in text.split('\n')) { + final trimmed = line.trim(); + if (trimmed.isEmpty || trimmed.startsWith('#')) continue; + final eqIndex = trimmed.indexOf('='); + if (eqIndex <= 0) continue; + final k = trimmed.substring(0, eqIndex).trim(); + final v = trimmed.substring(eqIndex + 1).trim(); + if (k.isNotEmpty) result[k] = v; } - return prefs.getInt(_keyBufferSize) ?? 0; // 0 = Auto - } - - // Hardware Decoding - Future setEnableHardwareDecoding(bool enabled) async { - await prefs.setBool(_keyEnableHardwareDecoding, enabled); - } - - bool getEnableHardwareDecoding() { - return prefs.getBool(_keyEnableHardwareDecoding) ?? true; // Default enabled - } - - // HDR (High Dynamic Range) - Future setEnableHDR(bool enabled) async { - await prefs.setBool(_keyEnableHDR, enabled); - } - - bool getEnableHDR() { - return prefs.getBool(_keyEnableHDR) ?? true; // Default enabled - } - - // Preferred Video Codec - Future setPreferredVideoCodec(String codec) async { - await prefs.setString(_keyPreferredVideoCodec, codec); - } - - String getPreferredVideoCodec() { - return prefs.getString(_keyPreferredVideoCodec) ?? 'auto'; - } - - // Preferred Audio Codec - Future setPreferredAudioCodec(String codec) async { - await prefs.setString(_keyPreferredAudioCodec, codec); - } - - String getPreferredAudioCodec() { - return prefs.getString(_keyPreferredAudioCodec) ?? 'auto'; - } - - // Library Density (int 1–5) - Future setLibraryDensity(int density) async { - await prefs.setInt(_keyLibraryDensity, density.clamp(LibraryDensity.min, LibraryDensity.max)); - } - - int getLibraryDensity() { - // New int format — getInt throws if the stored value is a different type - try { - final intVal = prefs.getInt(_keyLibraryDensity); - if (intVal != null) return intVal.clamp(LibraryDensity.min, LibraryDensity.max); - } on TypeError { - // Stored value is a String from old enum format — fall through to migration - } - // Migrate from old enum string format - String? strVal; - try { - strVal = prefs.getString(_keyLibraryDensity); - } on TypeError { - // Value exists but isn't a String either - } - final result = switch (strVal) { - 'compact' => 2, - 'comfortable' => 4, - _ => LibraryDensity.defaultValue, - }; - prefs.setInt(_keyLibraryDensity, result); return result; } - // View Mode - Future setViewMode(ViewMode mode) async { - await prefs.setString(_keyViewMode, mode.name); + /// Save a new preset (overwrites existing with same name). + Future saveMpvPreset(String name, String text) async { + final presets = read(mpvPresets).where((p) => p.name != name).toList(); + presets.add(MpvPreset(name: name, text: text, createdAt: DateTime.now())); + await write(mpvPresets, presets); } - ViewMode getViewMode() { - return _getEnumValue(_keyViewMode, ViewMode.values, ViewMode.grid); + Future deleteMpvPreset(String name) async { + final presets = read(mpvPresets).where((p) => p.name != name).toList(); + await write(mpvPresets, presets); } - // Episode Poster Mode - Future setEpisodePosterMode(EpisodePosterMode mode) async { - await prefs.setString(_keyEpisodePosterMode, mode.name); + /// Load a preset (replaces current config text). + Future loadMpvPreset(String name) async { + final presets = read(mpvPresets); + final preset = presets.firstWhere((p) => p.name == name, orElse: () => throw Exception('Preset not found: $name')); + await write(mpvConfigText, preset.text); } - EpisodePosterMode getEpisodePosterMode() { - // Migration: check old boolean key first - final legacyValue = prefs.getBool(_keyUseSeasonPoster); - if (legacyValue != null) { - // Migrate old setting: true = seasonPoster, false = seriesPoster - final migratedMode = legacyValue ? EpisodePosterMode.seasonPoster : EpisodePosterMode.seriesPoster; - // Clear old key and save new format (fire and forget) - prefs.remove(_keyUseSeasonPoster); - prefs.setString(_keyEpisodePosterMode, migratedMode.name); - return migratedMode; - } - return _getEnumValue(_keyEpisodePosterMode, EpisodePosterMode.values, EpisodePosterMode.episodeThumbnail); - } - - // Show Hero Section - Future setShowHeroSection(bool enabled) async { - await prefs.setBool(_keyShowHeroSection, enabled); - } - - bool getShowHeroSection() { - return prefs.getBool(_keyShowHeroSection) ?? true; // Default: true (show hero section) - } - - // Use Global Hubs (true = global /hubs endpoint, false = per-library hubs) - Future setUseGlobalHubs(bool enabled) async { - await prefs.setBool(_keyUseGlobalHubs, enabled); - } - - bool getUseGlobalHubs() { - return prefs.getBool(_keyUseGlobalHubs) ?? true; // Default: true (use global hubs like official Plex) - } - - // Show Server Name on Hubs (false = only on duplicates, true = always) - Future setShowServerNameOnHubs(bool enabled) async { - await prefs.setBool(_keyShowServerNameOnHubs, enabled); - } - - bool getShowServerNameOnHubs() { - return prefs.getBool(_keyShowServerNameOnHubs) ?? false; // Default: false (only show on duplicates) - } - - // Group Libraries by Server in the side rail - Future setGroupLibrariesByServer(bool enabled) async { - await prefs.setBool(_keyGroupLibrariesByServer, enabled); - } - - bool getGroupLibrariesByServer() { - return prefs.getBool(_keyGroupLibrariesByServer) ?? true; - } - - // Seek Time Small (in seconds) - Future setSeekTimeSmall(int seconds) async { - await prefs.setInt(_keySeekTimeSmall, seconds); - } - - int getSeekTimeSmall() { - return prefs.getInt(_keySeekTimeSmall) ?? 10; // Default: 10 seconds - } - - // Seek Time Large (in seconds) - Future setSeekTimeLarge(int seconds) async { - await prefs.setInt(_keySeekTimeLarge, seconds); - } - - int getSeekTimeLarge() { - return prefs.getInt(_keySeekTimeLarge) ?? 30; // Default: 30 seconds - } - - // Rewind on Resume (in seconds, 0 = disabled) - Future setRewindOnResume(int seconds) async { - await prefs.setInt(_keyRewindOnResume, seconds); - } - - int getRewindOnResume() { - return prefs.getInt(_keyRewindOnResume) ?? 0; // Default: 0 (disabled) - } - - // Sleep Timer Duration (in minutes) - Future setSleepTimerDuration(int minutes) async { - await prefs.setInt(_keySleepTimerDuration, minutes); - } - - int getSleepTimerDuration() { - return prefs.getInt(_keySleepTimerDuration) ?? 30; // Default: 30 minutes - } - - // Audio Sync Offset (in milliseconds) - Future setAudioSyncOffset(int milliseconds) async { - await prefs.setInt(_keyAudioSyncOffset, milliseconds); - } - - int getAudioSyncOffset() { - return prefs.getInt(_keyAudioSyncOffset) ?? 0; // Default: 0ms (no offset) - } - - // Subtitle Sync Offset (in milliseconds) - Future setSubtitleSyncOffset(int milliseconds) async { - await prefs.setInt(_keySubtitleSyncOffset, milliseconds); - } - - int getSubtitleSyncOffset() { - return prefs.getInt(_keySubtitleSyncOffset) ?? 0; // Default: 0ms (no offset) - } - - // Volume (0.0 to 100.0) - Future setVolume(double volume) async { - await prefs.setDouble(_keyVolume, volume); - } - - double getVolume() { - return prefs.getDouble(_keyVolume) ?? 100.0; // Default: full volume - } - - // Max Volume (100-300%, for volume boost) - Future setMaxVolume(int percent) async { - await prefs.setInt(_keyMaxVolume, percent.clamp(100, 300)); - } - - int getMaxVolume() { - return prefs.getInt(_keyMaxVolume) ?? 100; // Default: 100% (no boost) - } - - // Rotation Lock (mobile only) - Future setRotationLocked(bool locked) async { - await prefs.setBool(_keyRotationLocked, locked); - } - - bool getRotationLocked() { - return prefs.getBool(_keyRotationLocked) ?? true; // Default: locked (landscape only) - } - - // Subtitle Styling Settings - - // Font Size (30-80, default 55) - Future setSubtitleFontSize(int size) async { - await prefs.setInt(_keySubtitleFontSize, size); - } - - int getSubtitleFontSize() { - return prefs.getInt(_keySubtitleFontSize) ?? 38; - } - - // Text Color (hex format #RRGGBB, default white) - Future setSubtitleTextColor(String color) async { - await prefs.setString(_keySubtitleTextColor, color); - } - - String getSubtitleTextColor() { - return prefs.getString(_keySubtitleTextColor) ?? '#FFFFFF'; - } - - // Border Size (0-5, default 3) - Future setSubtitleBorderSize(int size) async { - await prefs.setInt(_keySubtitleBorderSize, size); - } - - int getSubtitleBorderSize() { - return prefs.getInt(_keySubtitleBorderSize) ?? 3; - } - - // Border Color (hex format #RRGGBB, default black) - Future setSubtitleBorderColor(String color) async { - await prefs.setString(_keySubtitleBorderColor, color); - } - - String getSubtitleBorderColor() { - return prefs.getString(_keySubtitleBorderColor) ?? '#000000'; - } - - // Background Color (hex format #RRGGBB, default black) - Future setSubtitleBackgroundColor(String color) async { - await prefs.setString(_keySubtitleBackgroundColor, color); - } - - String getSubtitleBackgroundColor() { - return prefs.getString(_keySubtitleBackgroundColor) ?? '#000000'; - } - - // Background Opacity (0-100, default 0 for transparent) - Future setSubtitleBackgroundOpacity(int opacity) async { - await prefs.setInt(_keySubtitleBackgroundOpacity, opacity); - } - - int getSubtitleBackgroundOpacity() { - return prefs.getInt(_keySubtitleBackgroundOpacity) ?? 0; - } - - // Subtitle Position (0 = top, 100 = bottom, default 100) - Future setSubtitlePosition(int position) async { - await prefs.setInt(_keySubtitlePosition, position.clamp(0, 100)); - } - - int getSubtitlePosition() { - return prefs.getInt(_keySubtitlePosition) ?? 100; // Default: bottom - } - - // Sub ASS Override - Future setSubAssOverride(SubAssOverride mode) async { - await prefs.setString(_keySubAssOverride, mode.name); - } - - SubAssOverride getSubAssOverride() { - return _getEnumValue(_keySubAssOverride, SubAssOverride.values, SubAssOverride.no); - } - - // Bold subtitles - Future setSubtitleBold(bool bold) async { - await prefs.setBool(_keySubtitleBold, bold); - } - - bool getSubtitleBold() { - return prefs.getBool(_keySubtitleBold) ?? false; - } - - // Italic subtitles - Future setSubtitleItalic(bool italic) async { - await prefs.setBool(_keySubtitleItalic, italic); - } - - bool getSubtitleItalic() { - return prefs.getBool(_keySubtitleItalic) ?? false; - } - - // Keyboard Shortcuts (Legacy String-based) - Map getDefaultKeyboardShortcuts() { - return { - 'play_pause': 'Space', - 'volume_up': 'Arrow Up', - 'volume_down': 'Arrow Down', - 'seek_forward': 'Arrow Right', - 'seek_backward': 'Arrow Left', - 'seek_forward_large': 'Shift+Arrow Right', - 'seek_backward_large': 'Shift+Arrow Left', - 'fullscreen_toggle': 'F', - 'mute_toggle': 'M', - 'subtitle_toggle': 'S', - 'audio_track_next': 'A', - 'subtitle_track_next': 'Shift+S', - 'chapter_next': 'N', - 'chapter_previous': 'P', - 'speed_increase': 'Plus', - 'speed_decrease': 'Minus', - 'speed_reset': 'R', - 'sub_seek_next': 'Ctrl+Right', - 'sub_seek_prev': 'Ctrl+Left', - }; - } - - // HotKey Objects (New implementation) - Map getDefaultKeyboardHotkeys() { - return { - 'play_pause': const HotKey(key: PhysicalKeyboardKey.space), - 'volume_up': const HotKey(key: PhysicalKeyboardKey.arrowUp), - 'volume_down': const HotKey(key: PhysicalKeyboardKey.arrowDown), - 'seek_forward': const HotKey(key: PhysicalKeyboardKey.arrowRight), - 'seek_backward': const HotKey(key: PhysicalKeyboardKey.arrowLeft), - 'seek_forward_large': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]), - 'seek_backward_large': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]), - 'fullscreen_toggle': const HotKey(key: PhysicalKeyboardKey.keyF), - 'mute_toggle': const HotKey(key: PhysicalKeyboardKey.keyM), - 'subtitle_toggle': const HotKey(key: PhysicalKeyboardKey.keyS), - 'audio_track_next': const HotKey(key: PhysicalKeyboardKey.keyA), - 'subtitle_track_next': const HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]), - 'chapter_next': const HotKey(key: PhysicalKeyboardKey.keyN), - 'chapter_previous': const HotKey(key: PhysicalKeyboardKey.keyP), - 'episode_next': const HotKey(key: PhysicalKeyboardKey.keyN, modifiers: [HotKeyModifier.shift]), - 'episode_previous': const HotKey(key: PhysicalKeyboardKey.keyP, modifiers: [HotKeyModifier.shift]), - 'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal), - 'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus), - 'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR), - 'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]), - 'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]), - 'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG), - 'skip_marker': const HotKey(key: PhysicalKeyboardKey.enter), - }; - } - - Future setKeyboardShortcuts(Map shortcuts) async { - final jsonString = json.encode(shortcuts); - await prefs.setString(_keyKeyboardShortcuts, jsonString); - } - - Map getKeyboardShortcuts() { - final jsonString = prefs.getString(_keyKeyboardShortcuts); - if (jsonString == null) return getDefaultKeyboardShortcuts(); - - final decoded = decodeJsonStringToMap(jsonString); - if (decoded.isEmpty) return getDefaultKeyboardShortcuts(); - - final shortcuts = decoded.map((key, value) => MapEntry(key, value.toString())); - - // Merge with defaults to ensure all keys exist - final defaults = getDefaultKeyboardShortcuts(); - defaults.addAll(shortcuts); - return defaults; - } - - Future setKeyboardShortcut(String action, String key) async { - final shortcuts = getKeyboardShortcuts(); - shortcuts[action] = key; - await setKeyboardShortcuts(shortcuts); - } - - String getKeyboardShortcut(String action) { - final shortcuts = getKeyboardShortcuts(); - return shortcuts[action] ?? ''; - } - - Future resetKeyboardShortcuts() async { - await setKeyboardShortcuts(getDefaultKeyboardShortcuts()); - } - - // HotKey Objects Methods - Future setKeyboardHotkeys(Map hotkeys) async { - final Map> serializedHotkeys = {}; - for (final entry in hotkeys.entries) { - serializedHotkeys[entry.key] = _serializeHotKey(entry.value); - } - final jsonString = json.encode(serializedHotkeys); - await prefs.setString(_keyKeyboardHotkeys, jsonString); - } - - Future> getKeyboardHotkeys() async { - final jsonString = prefs.getString(_keyKeyboardHotkeys); - if (jsonString == null) { - return getDefaultKeyboardHotkeys(); - } - - try { - final decoded = json.decode(jsonString) as Map; - final Map hotkeys = {}; - - for (final entry in decoded.entries) { - final hotKey = _deserializeHotKey(entry.value as Map); - if (hotKey != null) { - hotkeys[entry.key] = hotKey; - } - } - - // Merge with defaults to ensure all keys exist, but keep saved hotkeys priority - final defaults = getDefaultKeyboardHotkeys(); - final result = {}; - - // Start with defaults - result.addAll(defaults); - // Override with saved hotkeys (this preserves user customizations) - result.addAll(hotkeys); - - return result; - } catch (e) { - appLogger.d('Failed to parse keyboard hotkeys', error: e); - return getDefaultKeyboardHotkeys(); - } - } - - Future setKeyboardHotkey(String action, HotKey hotKey) async { - final hotkeys = await getKeyboardHotkeys(); - hotkeys[action] = hotKey; - await setKeyboardHotkeys(hotkeys); - } - - Future getKeyboardHotkey(String action) async { - final hotkeys = await getKeyboardHotkeys(); - return hotkeys[action]; - } - - Future resetKeyboardHotkeys() async { - await setKeyboardHotkeys(getDefaultKeyboardHotkeys()); - } - - // Video Player Navigation (use arrow keys to navigate video player controls) - Future setVideoPlayerNavigationEnabled(bool enabled) async { - await prefs.setBool(_keyVideoPlayerNavigationEnabled, enabled); - } - - bool getVideoPlayerNavigationEnabled() { - // Default: enabled on Android TV, disabled elsewhere - return prefs.getBool(_keyVideoPlayerNavigationEnabled) ?? TvDetectionService.isTVSync(); - } - - // Performance Overlay (show debug stats on video player) - Future setShowPerformanceOverlay(bool enabled) async { - await prefs.setBool(_keyShowPerformanceOverlay, enabled); - } - - bool getShowPerformanceOverlay() { - return prefs.getBool(_keyShowPerformanceOverlay) ?? false; // Default: disabled - } - - // Auto-hide performance overlay (fade with playback controls) - Future setAutoHidePerformanceOverlay(bool enabled) async { - await prefs.setBool(_keyAutoHidePerformanceOverlay, enabled); - } - - bool getAutoHidePerformanceOverlay() { - return prefs.getBool(_keyAutoHidePerformanceOverlay) ?? true; // Default: enabled - } - - // Helper methods for HotKey serialization static const _modifierMap = { 'alt': HotKeyModifier.alt, 'control': HotKeyModifier.control, @@ -699,36 +487,34 @@ class SettingsService extends BaseSharedPreferencesService { 'fn': HotKeyModifier.fn, }; - Map _serializeHotKey(HotKey hotKey) { - // Use USB HID code for reliable serialization across debug/release modes + static Map serializeHotKey(HotKey hotKey) { + // Use USB HID code for reliable serialization across debug/release modes. final usbHidCode = hotKey.key.usbHidUsage.toRadixString(16).padLeft(8, '0'); return {'key': usbHidCode, 'modifiers': hotKey.modifiers?.map((m) => m.name).toList() ?? []}; } - HotKey? _deserializeHotKey(Map data) { + static HotKey? deserializeHotKey(Map data) { try { final keyString = data['key'] as String; final modifierNames = (data['modifiers'] as List).cast(); - final modifiers = modifierNames .map((name) => _modifierMap[name]) .where((m) => m != null) .cast() .toList(); - - // Try parsing as USB HID code first (new format), fall back to string parsing (backwards compat) + // Try parsing as USB HID code first (new format), fall back to string parsing. final usbHidCode = int.tryParse(keyString, radix: 16); final key = usbHidCode != null ? PhysicalKeyboardKey(usbHidCode) : _findKeyByString(keyString); if (key != null) { return HotKey(key: key, modifiers: modifiers.isNotEmpty ? modifiers : null); } - } catch (e) { - // Ignore deserialization errors + } catch (_) { + // Ignore deserialization errors. } return null; } - // Map for pattern-based key name matching (lowercase keys for case-insensitive matching) + // Pattern-based key name matching (lowercase keys for case-insensitive matching). static const _keyNameMap = { 'space': PhysicalKeyboardKey.space, 'backspace': PhysicalKeyboardKey.backspace, @@ -749,7 +535,6 @@ class SettingsService extends BaseSharedPreferencesService { 'minus': PhysicalKeyboardKey.minus, }; - // Function keys map static const _functionKeyMap = { 'f1': PhysicalKeyboardKey.f1, 'f2': PhysicalKeyboardKey.f2, @@ -765,7 +550,6 @@ class SettingsService extends BaseSharedPreferencesService { 'f12': PhysicalKeyboardKey.f12, }; - // Digit keys map static const _digitKeyMap = { 'digit0': PhysicalKeyboardKey.digit0, 'digit1': PhysicalKeyboardKey.digit1, @@ -779,7 +563,6 @@ class SettingsService extends BaseSharedPreferencesService { 'digit9': PhysicalKeyboardKey.digit9, }; - // Letter keys map static const _letterKeyMap = { 'keya': PhysicalKeyboardKey.keyA, 'keyb': PhysicalKeyboardKey.keyB, @@ -809,840 +592,125 @@ class SettingsService extends BaseSharedPreferencesService { 'keyz': PhysicalKeyboardKey.keyZ, }; - // Helper method to find PhysicalKeyboardKey by string representation - PhysicalKeyboardKey? _findKeyByString(String keyString) { + static PhysicalKeyboardKey? _findKeyByString(String keyString) { final normalized = keyString.toLowerCase(); - // Try extracting USB HID code from toString() output - // Format: PhysicalKeyboardKey#ec9ed(usbHidUsage: "0x0007002c", debugName: "Space") + // Try extracting USB HID code from toString() output: + // PhysicalKeyboardKey#ec9ed(usbHidUsage: "0x0007002c", debugName: "Space") final usbHidMatch = RegExp(r'usbhidusage: "0x([0-9a-f]+)"').firstMatch(normalized); if (usbHidMatch != null) { final code = int.tryParse(usbHidMatch.group(1)!, radix: 16); if (code != null) return PhysicalKeyboardKey(code); } - // Try direct name matches for (final entry in _keyNameMap.entries) { - if (normalized.contains(entry.key)) { - return entry.value; - } + if (normalized.contains(entry.key)) return entry.value; } - - // Try function keys (check longer patterns first to avoid f1 matching f10) + // Function keys before digits to avoid f1 matching f10. for (final entry in _functionKeyMap.entries) { - if (normalized.contains(entry.key)) { - return entry.value; - } + if (normalized.contains(entry.key)) return entry.value; } - - // Try digit keys for (final entry in _digitKeyMap.entries) { - if (normalized.contains(entry.key)) { - return entry.value; - } + if (normalized.contains(entry.key)) return entry.value; } - - // Try letter keys for (final entry in _letterKeyMap.entries) { - if (normalized.contains(entry.key)) { - return entry.value; - } + if (normalized.contains(entry.key)) return entry.value; } return null; } - // Media Version Preferences - /// Save media version preference for a series - /// [seriesRatingKey] is the grandparentRatingKey for TV series, or ratingKey for movies - /// [mediaIndex] is the index of the selected media version - Future setMediaVersionPreference(String seriesRatingKey, int mediaIndex) async { - final preferences = _getMediaVersionPreferences(); - preferences[seriesRatingKey] = mediaIndex; + /// Settings that "Reset All Settings" actually resets. Mirrors the original + /// reset surface — notably excludes user-customized data (intro/credits regex + /// patterns) and opt-in toggles prior versions didn't reset, so behavior + /// stays identical for users. + static List _resettableKeys() => [ + enableDebugLogging.key, + bufferSize.key, + enableHardwareDecoding.key, + enableHDR.key, + preferredVideoCodec.key, + preferredAudioCodec.key, + viewMode.key, + showHeroSection.key, + seekTimeSmall.key, + seekTimeLarge.key, + sleepTimerDuration.key, + audioSyncOffset.key, + subtitleSyncOffset.key, + volume.key, + maxVolume.key, + subtitleFontSize.key, + subtitleTextColor.key, + subtitleBorderSize.key, + subtitleBorderColor.key, + subtitleBackgroundColor.key, + subtitleBackgroundOpacity.key, + subtitlePosition.key, + rememberTrackSelections.key, + customDownloadPathType.key, + downloadOnWifiOnly.key, + autoCheckUpdatesOnStartup.key, + showPerformanceOverlay.key, + autoHidePerformanceOverlay.key, + enableDiscordRPC.key, + enableTraktScrobble.key, + enableTraktWatchedSync.key, + enableMalScrobble.key, + enableAnilistScrobble.key, + enableSimklScrobble.key, + matchContentFrameRate.key, + tunneledPlayback.key, + defaultPlaybackSpeed.key, + defaultBoxFitMode.key, + autoPlayNextEpisode.key, + useExoPlayer.key, + alwaysKeepSidebarOpen.key, + showUnwatchedCount.key, + showEpisodeNumberOnCards.key, + hideSpoilers.key, + showNavBarLabels.key, + globalShaderPreset.key, + requireProfileSelectionOnOpen.key, + useExternalPlayer.key, + confirmExitOnBack.key, + forceTvMode.key, + ambientLighting.key, + audioPassthrough.key, + audioNormalization.key, + themeMode.key, + keyboardShortcuts.key, + keyboardHotkeys.key, + libraryDensity.key, + _legacyUseSeasonPosterKey, + episodePosterMode.key, + mediaVersionPreferences.key, + appLocale.key, + customDownloadPath.key, + videoPlayerNavigationEnabled.key, + _legacyMpvConfigEntriesKey, + mpvConfigText.key, + mpvPresets.key, + autoPip.key, + customShaderPresets.key, + selectedExternalPlayer.key, + customExternalPlayers.key, + _bufferSizeMigratedKey, + customRelayUrl.key, + ]; - final jsonString = json.encode(preferences); - await prefs.setString(_keyMediaVersionPreferences, jsonString); - } - - /// Get saved media version preference for a series - /// Returns null if no preference is saved - int? getMediaVersionPreference(String seriesRatingKey) { - final preferences = _getMediaVersionPreferences(); - return preferences[seriesRatingKey]; - } - - /// Get all media version preferences - Map _getMediaVersionPreferences() { - final jsonString = prefs.getString(_keyMediaVersionPreferences); - if (jsonString == null) return {}; - - final decoded = decodeJsonStringToMap(jsonString); - return decoded.map((key, value) => MapEntry(key, value as int)); - } - - // App Locale - Future setAppLocale(AppLocale locale) async { - await prefs.setString(_keyAppLocale, locale.languageCode); - } - - AppLocale getAppLocale() { - final localeString = prefs.getString(_keyAppLocale); - if (localeString == null) return AppLocaleUtils.findDeviceLocale(); - - return AppLocale.values.asNameMap()[localeString] ?? AppLocale.en; - } - - // Track Selection Settings - - /// Remember Track Selections - Save per-media audio/subtitle language preferences - Future setRememberTrackSelections(bool value) async { - await prefs.setBool(_keyRememberTrackSelections, value); - } - - bool getRememberTrackSelections() { - return prefs.getBool(_keyRememberTrackSelections) ?? true; - } - - // Click on Video Player Settings - Future setClickVideoTogglesPlayback(bool value) async { - await prefs.setBool(_keyClickVideoTogglesPlayback, value); - } - - bool getClickVideoTogglesPlayback() { - return prefs.getBool(_keyClickVideoTogglesPlayback) ?? false; - } - - // Auto Skip Intro - Future setAutoSkipIntro(bool value) async { - await prefs.setBool(_keyAutoSkipIntro, value); - } - - bool getAutoSkipIntro() { - return prefs.getBool(_keyAutoSkipIntro) ?? false; // Default: disabled - } - - // Auto Skip Credits - Future setAutoSkipCredits(bool value) async { - await prefs.setBool(_keyAutoSkipCredits, value); - } - - bool getAutoSkipCredits() { - return prefs.getBool(_keyAutoSkipCredits) ?? false; // Default: disabled - } - - // Auto Skip Delay (in seconds) - Future setAutoSkipDelay(int seconds) async { - await prefs.setInt(_keyAutoSkipDelay, seconds); - } - - int getAutoSkipDelay() { - return prefs.getInt(_keyAutoSkipDelay) ?? 5; // Default: 5 seconds - } - - // Intro Pattern - Future setIntroPattern(String value) async { - await prefs.setString(_keyIntroPattern, value); - } - - String getIntroPattern() { - return prefs.getString(_keyIntroPattern) ?? defaultIntroPattern; - } - - // Credits Pattern - Future setCreditsPattern(String value) async { - await prefs.setString(_keyCreditsPattern, value); - } - - String getCreditsPattern() { - return prefs.getString(_keyCreditsPattern) ?? defaultCreditsPattern; - } - - // Custom Download Path - Future setCustomDownloadPath(String? path, {String type = 'file'}) async { - if (path == null) { - await prefs.remove(_keyCustomDownloadPath); - await prefs.remove(_keyCustomDownloadPathType); - } else { - await prefs.setString(_keyCustomDownloadPath, path); - await prefs.setString(_keyCustomDownloadPathType, type); - } - } - - String? getCustomDownloadPath() { - return prefs.getString(_keyCustomDownloadPath); - } - - String getCustomDownloadPathType() { - return prefs.getString(_keyCustomDownloadPathType) ?? 'file'; - } - - bool hasCustomDownloadPath() { - return prefs.containsKey(_keyCustomDownloadPath); - } - - // Custom Relay URL - Future setCustomRelayUrl(String? url) async { - if (url == null || url.trim().isEmpty) { - await prefs.remove(_keyCustomRelayUrl); - } else { - await prefs.setString(_keyCustomRelayUrl, url.trim()); - } - } - - String? getCustomRelayUrl() { - return prefs.getString(_keyCustomRelayUrl); - } - - // Recent Watch Together rooms - Future setRecentRooms(String json) async { - await prefs.setString(_keyRecentRooms, json); - } - - String? getRecentRooms() { - return prefs.getString(_keyRecentRooms); - } - - // Download on WiFi Only - Future setDownloadOnWifiOnly(bool value) async { - await prefs.setBool(_keyDownloadOnWifiOnly, value); - } - - bool getDownloadOnWifiOnly() { - return prefs.getBool(_keyDownloadOnWifiOnly) ?? false; - } - - Future setAutoCheckUpdatesOnStartup(bool value) async { - await prefs.setBool(_keyAutoCheckUpdatesOnStartup, value); - } - - bool getAutoCheckUpdatesOnStartup() { - return prefs.getBool(_keyAutoCheckUpdatesOnStartup) ?? true; - } - - // Auto-remove watched downloads - Future setAutoRemoveWatchedDownloads(bool value) async { - await prefs.setBool(_keyAutoRemoveWatchedDownloads, value); - } - - bool getAutoRemoveWatchedDownloads() { - return prefs.getBool(_keyAutoRemoveWatchedDownloads) ?? false; - } - - // Per-server watched threshold (cached from server prefs) - Future setWatchedThreshold(String serverId, int percent) async { - await prefs.setInt('$_prefixWatchedThreshold$serverId', percent); - } - - int getWatchedThreshold(String serverId) { - return prefs.getInt('$_prefixWatchedThreshold$serverId') ?? 90; - } - - // MPV Config (raw text) - - /// Get the raw MPV config text. Migrates from legacy JSON entries on first read. - String getMpvConfigText() { - final text = prefs.getString(_keyMpvConfigText); - if (text != null) return text; - - // Migrate from legacy JSON entries - final legacyJson = prefs.getString(_keyMpvConfigEntries); - if (legacyJson != null) { - try { - final List decoded = json.decode(legacyJson); - final lines = []; - for (final item in decoded) { - if (item is Map) { - final key = item['key'] as String? ?? ''; - final value = item['value'] as String? ?? ''; - final enabled = item['isEnabled'] as bool? ?? true; - if (key.isNotEmpty) { - lines.add(enabled ? '$key=$value' : '#$key=$value'); - } - } - } - final migrated = lines.join('\n'); - prefs.setString(_keyMpvConfigText, migrated); - return migrated; - } catch (e, st) { - appLogger.w('SettingsService: failed to migrate mpv config', error: e, stackTrace: st); - } - } - - return ''; - } - - /// Save the raw MPV config text - Future setMpvConfigText(String text) async { - await prefs.setString(_keyMpvConfigText, text); - } - - /// Parse raw config text into a `Map` (skip blanks and # comments) - static Map parseMpvConfigText(String text) { - final result = {}; - for (final line in text.split('\n')) { - final trimmed = line.trim(); - if (trimmed.isEmpty || trimmed.startsWith('#')) continue; - final eqIndex = trimmed.indexOf('='); - if (eqIndex <= 0) continue; - final key = trimmed.substring(0, eqIndex).trim(); - final value = trimmed.substring(eqIndex + 1).trim(); - if (key.isNotEmpty) result[key] = value; - } - return result; - } - - /// Get enabled MPV config entries (for player initialization) - Map getEnabledMpvConfigEntries() { - return parseMpvConfigText(getMpvConfigText()); - } - - // MPV Presets - - /// Get all saved presets - List getMpvPresets() { - final jsonString = prefs.getString(_keyMpvConfigPresets); - if (jsonString == null) return []; - - try { - final List decoded = json.decode(jsonString); - return decoded.map((e) { - final map = e as Map; - // Migrate legacy presets with entries list to text - if (map.containsKey('entries') && !map.containsKey('text')) { - final entries = map['entries'] as List; - final lines = []; - for (final item in entries) { - if (item is Map) { - final key = item['key'] as String? ?? ''; - final value = item['value'] as String? ?? ''; - final enabled = item['isEnabled'] as bool? ?? true; - if (key.isNotEmpty) { - lines.add(enabled ? '$key=$value' : '#$key=$value'); - } - } - } - map['text'] = lines.join('\n'); - } - return MpvPreset.fromJson(map); - }).toList(); - } catch (e) { - return []; - } - } - - /// Save a new preset (overwrites existing with same name) - Future saveMpvPreset(String name, String text) async { - final presets = getMpvPresets(); - presets.removeWhere((p) => p.name == name); - presets.add(MpvPreset(name: name, text: text, createdAt: DateTime.now())); - - final jsonString = json.encode(presets.map((p) => p.toJson()).toList()); - await prefs.setString(_keyMpvConfigPresets, jsonString); - } - - /// Delete a preset by name - Future deleteMpvPreset(String name) async { - final presets = getMpvPresets(); - presets.removeWhere((p) => p.name == name); - - final jsonString = json.encode(presets.map((p) => p.toJson()).toList()); - await prefs.setString(_keyMpvConfigPresets, jsonString); - } - - /// Load a preset (replaces current config text) - Future loadMpvPreset(String name) async { - final presets = getMpvPresets(); - final preset = presets.firstWhere((p) => p.name == name, orElse: () => throw Exception('Preset not found: $name')); - - await setMpvConfigText(preset.text); - } - - // Discord Rich Presence - Future setEnableDiscordRPC(bool enabled) async { - await prefs.setBool(_keyEnableDiscordRPC, enabled); - } - - bool getEnableDiscordRPC() { - return prefs.getBool(_keyEnableDiscordRPC) ?? false; // Default disabled - } - - // Trakt Scrobbling (sends play/pause/stop during playback to Trakt) - Future setEnableTraktScrobble(bool enabled) async { - await prefs.setBool(_keyEnableTraktScrobble, enabled); - } - - bool getEnableTraktScrobble() { - return prefs.getBool(_keyEnableTraktScrobble) ?? true; // Default enabled when account is linked - } - - // Trakt Watched-Status Sync (Plezy → Trakt only) - Future setEnableTraktWatchedSync(bool enabled) async { - await prefs.setBool(_keyEnableTraktWatchedSync, enabled); - } - - bool getEnableTraktWatchedSync() { - return prefs.getBool(_keyEnableTraktWatchedSync) ?? true; // Default enabled when account is linked - } - - // Tracker scrobbling (threshold-based, fires at 80% progress) - Future setEnableMalScrobble(bool enabled) async { - await prefs.setBool(_keyEnableMalScrobble, enabled); - } - - bool getEnableMalScrobble() { - return prefs.getBool(_keyEnableMalScrobble) ?? true; - } - - Future setEnableAnilistScrobble(bool enabled) async { - await prefs.setBool(_keyEnableAnilistScrobble, enabled); - } - - bool getEnableAnilistScrobble() { - return prefs.getBool(_keyEnableAnilistScrobble) ?? true; - } - - Future setEnableSimklScrobble(bool enabled) async { - await prefs.setBool(_keyEnableSimklScrobble, enabled); - } - - bool getEnableSimklScrobble() { - return prefs.getBool(_keyEnableSimklScrobble) ?? true; - } - - // Per-tracker library filter. Default blacklist+empty = sync every library. - TrackerLibraryFilterMode getTrackerLibraryFilterMode(TrackerService service) => _getEnumValue( - '$_prefixTrackerLibraryFilterMode${service.name}', - TrackerLibraryFilterMode.values, - TrackerLibraryFilterMode.blacklist, - ); - - Future setTrackerLibraryFilterMode(TrackerService service, TrackerLibraryFilterMode mode) async { - await prefs.setString('$_prefixTrackerLibraryFilterMode${service.name}', mode.name); - } - - Set getTrackerLibraryFilterIds(TrackerService service) { - final ids = prefs.getStringList('$_prefixTrackerLibraryFilterIds${service.name}'); - return ids?.toSet() ?? {}; - } - - Future setTrackerLibraryFilterIds(TrackerService service, Set ids) async { - await prefs.setStringList('$_prefixTrackerLibraryFilterIds${service.name}', ids.toList()); - } - - /// Whether a scrobble/sync for [libraryGlobalKey] should be sent to [service]. - /// Null keys bypass the filter so we err on the side of syncing. - bool isLibraryAllowedForTracker(TrackerService service, String? libraryGlobalKey) { - if (libraryGlobalKey == null) return true; - final inList = getTrackerLibraryFilterIds(service).contains(libraryGlobalKey); - return getTrackerLibraryFilterMode(service) == TrackerLibraryFilterMode.blacklist ? !inList : inList; - } - - // Companion Remote Server - Future setEnableCompanionRemoteServer(bool enabled) async { - await prefs.setBool(_keyEnableCompanionRemoteServer, enabled); - } - - bool getEnableCompanionRemoteServer() { - // Default enabled on desktop/TV, disabled on mobile - return prefs.getBool(_keyEnableCompanionRemoteServer) ?? (PlatformDetector.isDesktopOS()); - } - - // Auto Picture-in-Picture (Android & iOS) - Future setAutoPip(bool enabled) async { - await prefs.setBool(_keyAutoPip, enabled); - } - - bool getAutoPip() { - if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) return false; - if (PlatformDetector.isTV()) return false; - // Default enabled on mobile, disabled on macOS - return prefs.getBool(_keyAutoPip) ?? !Platform.isMacOS; - } - - // Match Content Frame Rate (Android only) - Future setMatchContentFrameRate(bool enabled) async { - await prefs.setBool(_keyMatchContentFrameRate, enabled); - } - - bool getMatchContentFrameRate() { - return prefs.getBool(_keyMatchContentFrameRate) ?? false; // Default disabled - } - - // Match Refresh Rate (Windows) - Future setMatchRefreshRate(bool enabled) async { - await prefs.setBool(_keyMatchRefreshRate, enabled); - } - - bool getMatchRefreshRate() { - return prefs.getBool(_keyMatchRefreshRate) ?? false; - } - - // Match Dynamic Range (Windows) - Future setMatchDynamicRange(bool enabled) async { - await prefs.setBool(_keyMatchDynamicRange, enabled); - } - - bool getMatchDynamicRange() { - return prefs.getBool(_keyMatchDynamicRange) ?? false; - } - - // Display Switch Delay (Windows, seconds) - Future setDisplaySwitchDelay(int seconds) async { - await prefs.setInt(_keyDisplaySwitchDelay, seconds.clamp(0, 10)); - } - - int getDisplaySwitchDelay() { - return prefs.getInt(_keyDisplaySwitchDelay) ?? 0; - } - - // Tunneled Playback (Android ExoPlayer only) - Future setTunneledPlayback(bool enabled) async { - await prefs.setBool(_keyTunneledPlayback, enabled); - } - - bool getTunneledPlayback() { - return prefs.getBool(_keyTunneledPlayback) ?? true; // Default: enabled - } - - // Default Playback Speed (0.5 to 3.0) - Future setDefaultPlaybackSpeed(double speed) async { - await prefs.setDouble(_keyDefaultPlaybackSpeed, speed.clamp(0.5, 3.0)); - } - - double getDefaultPlaybackSpeed() { - return prefs.getDouble(_keyDefaultPlaybackSpeed) ?? 1.0; // Default: normal speed - } - - // Default BoxFit Mode (0=contain, 1=cover, 2=fill) - Future setDefaultBoxFitMode(int mode) async { - await prefs.setInt(_keyDefaultBoxFitMode, mode.clamp(0, 2)); - } - - int getDefaultBoxFitMode() { - return prefs.getInt(_keyDefaultBoxFitMode) ?? 0; // Default: contain - } - - // Auto-Play Next Episode - Future setAutoPlayNextEpisode(bool enabled) async { - await prefs.setBool(_keyAutoPlayNextEpisode, enabled); - } - - bool getAutoPlayNextEpisode() { - return prefs.getBool(_keyAutoPlayNextEpisode) ?? true; // Default enabled - } - - // Use ExoPlayer on Android (default: true) - // When false, uses MPV as the player backend - Future setUseExoPlayer(bool enabled) async { - await prefs.setBool(_keyUseExoPlayer, enabled); - } - - bool getUseExoPlayer() { - return prefs.getBool(_keyUseExoPlayer) ?? true; // Default: ExoPlayer - } - - // Always Keep Sidebar Open (Desktop/TV only) - Future setAlwaysKeepSidebarOpen(bool enabled) async { - await prefs.setBool(_keyAlwaysKeepSidebarOpen, enabled); - } - - bool getAlwaysKeepSidebarOpen() { - return prefs.getBool(_keyAlwaysKeepSidebarOpen) ?? false; // Default: collapsed - } - - // Show Unwatched Count (show unwatched episode count on shows/seasons) - Future setShowUnwatchedCount(bool enabled) async { - await prefs.setBool(_keyShowUnwatchedCount, enabled); - } - - bool getShowUnwatchedCount() { - return prefs.getBool(_keyShowUnwatchedCount) ?? true; // Default: enabled (show counts) - } - - // Show Episode Number on Cards (append E# alongside S# on episode cards) - Future setShowEpisodeNumberOnCards(bool enabled) async { - await prefs.setBool(_keyShowEpisodeNumberOnCards, enabled); - } - - bool getShowEpisodeNumberOnCards() { - return prefs.getBool(_keyShowEpisodeNumberOnCards) ?? true; // Default: enabled - } - - // Hide Spoilers (blur thumbnails and hide descriptions for unwatched episodes) - Future setHideSpoilers(bool enabled) async { - await prefs.setBool(_keyHideSpoilers, enabled); - } - - bool getHideSpoilers() { - return prefs.getBool(_keyHideSpoilers) ?? false; // Default: disabled - } - - // Show Navigation Bar Labels (mobile bottom nav) - Future setShowNavBarLabels(bool enabled) async { - await prefs.setBool(_keyShowNavBarLabels, enabled); - } - - bool getShowNavBarLabels() { - return prefs.getBool(_keyShowNavBarLabels) ?? true; // Default: show labels - } - - Future setLiveTvDefaultFavorites(bool enabled) async { - await prefs.setBool(_keyLiveTvDefaultFavorites, enabled); - } - - bool getLiveTvDefaultFavorites() { - return prefs.getBool(_keyLiveTvDefaultFavorites) ?? false; - } - - // Global Shader Preset (for MPV video enhancement) - Future setGlobalShaderPreset(String presetId) async { - await prefs.setString(_keyGlobalShaderPreset, presetId); - } - - String getGlobalShaderPreset() { - return prefs.getString(_keyGlobalShaderPreset) ?? 'none'; // Default: no shader - } - - // Custom Shader Presets - List> getCustomShaderPresets() { - final jsonString = prefs.getString(_keyCustomShaderPresets); - if (jsonString == null) return []; - try { - final List decoded = json.decode(jsonString); - return decoded.cast>(); - } catch (_) { - return []; - } - } - - Future setCustomShaderPresets(List> presets) async { - await prefs.setString(_keyCustomShaderPresets, json.encode(presets)); - } - - // Require Profile Selection on App Open - Future setRequireProfileSelectionOnOpen(bool enabled) async { - await prefs.setBool(_keyRequireProfileSelectionOnOpen, enabled); - } - - bool getRequireProfileSelectionOnOpen() { - return prefs.getBool(_keyRequireProfileSelectionOnOpen) ?? false; - } - - // External Player - Future setUseExternalPlayer(bool enabled) async { - await prefs.setBool(_keyUseExternalPlayer, enabled); - } - - bool getUseExternalPlayer() { - return prefs.getBool(_keyUseExternalPlayer) ?? false; - } - - Future setSelectedExternalPlayer(ExternalPlayer player) async { - await prefs.setString(_keySelectedExternalPlayer, player.toJsonString()); - } - - ExternalPlayer getSelectedExternalPlayer() { - final jsonString = prefs.getString(_keySelectedExternalPlayer); - if (jsonString == null) return KnownPlayers.systemDefault; - try { - return ExternalPlayer.fromJsonString(jsonString); - } catch (e) { - appLogger.d('Failed to parse external player', error: e); - return KnownPlayers.systemDefault; - } - } - - Future setCustomExternalPlayers(List players) async { - final jsonString = json.encode(players.map((p) => p.toJson()).toList()); - await prefs.setString(_keyCustomExternalPlayers, jsonString); - } - - List getCustomExternalPlayers() { - final jsonString = prefs.getString(_keyCustomExternalPlayers); - if (jsonString == null) return []; - try { - final List decoded = json.decode(jsonString); - return decoded.map((e) => ExternalPlayer.fromJson(e as Map)).toList(); - } catch (e) { - appLogger.d('Failed to parse custom external players', error: e); - return []; - } - } - - Future addCustomExternalPlayer(ExternalPlayer player) async { - final players = getCustomExternalPlayers(); - players.add(player); - await setCustomExternalPlayers(players); - } - - Future removeCustomExternalPlayer(String id) async { - final players = getCustomExternalPlayers(); - players.removeWhere((p) => p.id == id); - await setCustomExternalPlayers(players); - // If the removed player was selected, reset to system default - if (getSelectedExternalPlayer().id == id) { - await setSelectedExternalPlayer(KnownPlayers.systemDefault); - } - } - - // Confirm Exit on Back (Android TV) - Future setConfirmExitOnBack(bool value) async { - await prefs.setBool(_keyConfirmExitOnBack, value); - } - - bool getConfirmExitOnBack() { - return prefs.getBool(_keyConfirmExitOnBack) ?? true; // Default: enabled - } - - // Force TV Mode (Android) - Future setForceTvMode(bool value) async { - await prefs.setBool(_keyForceTvMode, value); - } - - bool getForceTvMode() => prefs.getBool(_keyForceTvMode) ?? false; - - // Ambient Lighting - Future setAmbientLighting(bool enabled) async { - await prefs.setBool(_keyAmbientLighting, enabled); - } - - bool getAmbientLighting() { - return prefs.getBool(_keyAmbientLighting) ?? false; - } - - // Audio Passthrough - Future setAudioPassthrough(bool enabled) async { - await prefs.setBool(_keyAudioPassthrough, enabled); - } - - bool getAudioPassthrough() { - return prefs.getBool(_keyAudioPassthrough) ?? false; - } - - // Audio Normalization - Future setAudioNormalization(bool enabled) async { - await prefs.setBool(_keyAudioNormalization, enabled); - } - - bool getAudioNormalization() { - return prefs.getBool(_keyAudioNormalization) ?? false; - } - - // Reset all settings to defaults Future resetAllSettings() async { await Future.wait([ - prefs.remove(_keyThemeMode), - prefs.remove(_keyEnableDebugLogging), - prefs.remove(_keyBufferSize), - prefs.remove(_keyKeyboardShortcuts), - prefs.remove(_keyKeyboardHotkeys), - prefs.remove(_keyEnableHardwareDecoding), - prefs.remove(_keyEnableHDR), - prefs.remove(_keyPreferredVideoCodec), - prefs.remove(_keyPreferredAudioCodec), - prefs.remove(_keyLibraryDensity), - prefs.remove(_keyViewMode), - prefs.remove(_keyUseSeasonPoster), // Legacy key - prefs.remove(_keyEpisodePosterMode), - prefs.remove(_keyShowHeroSection), - prefs.remove(_keySeekTimeSmall), - prefs.remove(_keySeekTimeLarge), - prefs.remove(_keyMediaVersionPreferences), - prefs.remove(_keySleepTimerDuration), - prefs.remove(_keyAudioSyncOffset), - prefs.remove(_keySubtitleSyncOffset), - prefs.remove(_keyVolume), - prefs.remove(_keyMaxVolume), - prefs.remove(_keySubtitleFontSize), - prefs.remove(_keySubtitleTextColor), - prefs.remove(_keySubtitleBorderSize), - prefs.remove(_keySubtitleBorderColor), - prefs.remove(_keySubtitleBackgroundColor), - prefs.remove(_keySubtitleBackgroundOpacity), - prefs.remove(_keySubtitlePosition), - prefs.remove(_keyAppLocale), - prefs.remove(_keyRememberTrackSelections), - prefs.remove(_keyCustomDownloadPath), - prefs.remove(_keyCustomDownloadPathType), - prefs.remove(_keyDownloadOnWifiOnly), - prefs.remove(_keyAutoCheckUpdatesOnStartup), - prefs.remove(_keyVideoPlayerNavigationEnabled), - prefs.remove(_keyShowPerformanceOverlay), - prefs.remove(_keyAutoHidePerformanceOverlay), - prefs.remove(_keyMpvConfigEntries), - prefs.remove(_keyMpvConfigText), - prefs.remove(_keyMpvConfigPresets), - prefs.remove(_keyEnableDiscordRPC), - prefs.remove(_keyEnableTraktScrobble), - prefs.remove(_keyEnableTraktWatchedSync), - prefs.remove(_keyEnableMalScrobble), - prefs.remove(_keyEnableAnilistScrobble), - prefs.remove(_keyEnableSimklScrobble), + ..._resettableKeys().map((k) => prefs.remove(k)), ...TrackerService.values.expand( - (s) => [ - prefs.remove('$_prefixTrackerLibraryFilterMode${s.name}'), - prefs.remove('$_prefixTrackerLibraryFilterIds${s.name}'), - ], + (s) => [prefs.remove(trackerFilterModePref(s).key), prefs.remove(trackerFilterIdsPref(s).key)], ), - prefs.remove(_keyAutoPip), - prefs.remove(_keyMatchContentFrameRate), - prefs.remove(_keyTunneledPlayback), - prefs.remove(_keyDefaultPlaybackSpeed), - prefs.remove(_keyDefaultBoxFitMode), - prefs.remove(_keyAutoPlayNextEpisode), - prefs.remove(_keyUseExoPlayer), - prefs.remove(_keyAlwaysKeepSidebarOpen), - prefs.remove(_keyShowUnwatchedCount), - prefs.remove(_keyShowEpisodeNumberOnCards), - prefs.remove(_keyHideSpoilers), - prefs.remove(_keyShowNavBarLabels), - prefs.remove(_keyGlobalShaderPreset), - prefs.remove(_keyCustomShaderPresets), - prefs.remove(_keyRequireProfileSelectionOnOpen), - prefs.remove(_keyUseExternalPlayer), - prefs.remove(_keySelectedExternalPlayer), - prefs.remove(_keyCustomExternalPlayers), - prefs.remove(_keyConfirmExitOnBack), - prefs.remove(_keyForceTvMode), - prefs.remove(_keyAmbientLighting), - prefs.remove(_keyAudioPassthrough), - prefs.remove(_keyAudioNormalization), - prefs.remove(_keyBufferSizeMigratedToAuto), - prefs.remove(_keyCustomRelayUrl), ]); } - // Clear cache (for storage cleanup) Future clearCache() async { PaintingBinding.instance.imageCache.clear(); PaintingBinding.instance.imageCache.clearLiveImages(); await PlexImageCacheManager.instance.emptyCache(); } - - // Get all settings as a map for debugging/export - Future> getAllSettings() async { - final hotkeys = await getKeyboardHotkeys(); - return { - 'themeMode': getThemeMode().name, - 'enableDebugLogging': getEnableDebugLogging(), - 'bufferSize': getBufferSize(), - 'enableHardwareDecoding': getEnableHardwareDecoding(), - 'preferredVideoCodec': getPreferredVideoCodec(), - 'preferredAudioCodec': getPreferredAudioCodec(), - 'libraryDensity': getLibraryDensity(), - 'viewMode': getViewMode().name, - 'episodePosterMode': getEpisodePosterMode().name, - 'seekTimeSmall': getSeekTimeSmall(), - 'seekTimeLarge': getSeekTimeLarge(), - 'keyboardShortcuts': getKeyboardShortcuts(), - 'keyboardHotkeys': hotkeys.map((key, value) => MapEntry(key, _serializeHotKey(value))), - 'rememberTrackSelections': getRememberTrackSelections(), - 'clickVideoTogglesPlayback': getClickVideoTogglesPlayback(), - 'autoSkipIntro': getAutoSkipIntro(), - 'autoSkipCredits': getAutoSkipCredits(), - 'autoSkipDelay': getAutoSkipDelay(), - 'introPattern': getIntroPattern(), - 'creditsPattern': getCreditsPattern(), - }; - } } diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 78c80c02..b2fb03a6 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -368,7 +368,7 @@ class StorageService extends BaseSharedPreferencesService { /// Load all persisted episode counts Map loadAllEpisodeCounts() { final counts = {}; - final keys = prefs.getKeys().where((k) => k.startsWith(_prefixEpisodeCount)); + final keys = prefs.keys.where((k) => k.startsWith(_prefixEpisodeCount)); for (final key in keys) { final globalKey = key.replaceFirst(_prefixEpisodeCount, ''); @@ -415,7 +415,7 @@ class StorageService extends BaseSharedPreferencesService { /// Remove all keys matching a prefix Future _clearKeysWithPrefix(String prefix) async { - final keys = prefs.getKeys().where((k) => k.startsWith(prefix)); + final keys = prefs.keys.where((k) => k.startsWith(prefix)); await Future.wait(keys.map((k) => prefs.remove(k))); } diff --git a/lib/services/track_manager.dart b/lib/services/track_manager.dart index c367a9b7..cffad881 100644 --- a/lib/services/track_manager.dart +++ b/lib/services/track_manager.dart @@ -180,7 +180,7 @@ class TrackManager { preferredAudioTrack: preferredAudioTrack, preferredSubtitleTrack: preferredSubtitleTrack, preferredSecondarySubtitleTrack: preferredSecondarySubtitleTrack, - defaultPlaybackSpeed: settingsService.getDefaultPlaybackSpeed(), + defaultPlaybackSpeed: settingsService.read(SettingsService.defaultPlaybackSpeed), onAudioTrackChanged: onAudioTrackChanged, onSubtitleTrackChanged: onSubtitleTrackChanged, ); @@ -354,7 +354,7 @@ class TrackManager { /// Common guard checks for track change handlers. Future _guardTrackChange(PlexMediaInfo? info) async { final settings = await SettingsService.getInstance(); - if (!settings.getRememberTrackSelections()) return null; + if (!settings.read(SettingsService.rememberTrackSelections)) return null; if (info == null) { appLogger.w('No media info available, cannot save stream selection'); diff --git a/lib/services/trackers/anilist/anilist_tracker.dart b/lib/services/trackers/anilist/anilist_tracker.dart index a3d515d9..2f06c3da 100644 --- a/lib/services/trackers/anilist/anilist_tracker.dart +++ b/lib/services/trackers/anilist/anilist_tracker.dart @@ -30,7 +30,7 @@ class AnilistTracker extends TrackerBase { bool get hasActiveClient => _client != null; @override - bool readEnabledSetting(SettingsService settings) => settings.getEnableAnilistScrobble(); + bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableAnilistScrobble); void rebindSession(AnilistSession? session, {required void Function() onSessionInvalidated}) { _client?.dispose(); diff --git a/lib/services/trackers/mal/mal_tracker.dart b/lib/services/trackers/mal/mal_tracker.dart index 6acbee1c..de8391fc 100644 --- a/lib/services/trackers/mal/mal_tracker.dart +++ b/lib/services/trackers/mal/mal_tracker.dart @@ -35,7 +35,7 @@ class MalTracker extends TrackerBase { bool get hasActiveClient => _client != null; @override - bool readEnabledSetting(SettingsService settings) => settings.getEnableMalScrobble(); + bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableMalScrobble); void rebindSession( MalSession? session, { diff --git a/lib/services/trackers/simkl/simkl_tracker.dart b/lib/services/trackers/simkl/simkl_tracker.dart index 7aa77645..94c3e1dc 100644 --- a/lib/services/trackers/simkl/simkl_tracker.dart +++ b/lib/services/trackers/simkl/simkl_tracker.dart @@ -33,7 +33,7 @@ class SimklTracker extends TrackerBase { bool get hasActiveClient => _client != null; @override - bool readEnabledSetting(SettingsService settings) => settings.getEnableSimklScrobble(); + bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableSimklScrobble); void rebindSession(SimklSession? session, {required void Function() onSessionInvalidated}) { _client?.dispose(); diff --git a/lib/services/trakt/trakt_scrobble_service.dart b/lib/services/trakt/trakt_scrobble_service.dart index 13092218..abca3e80 100644 --- a/lib/services/trakt/trakt_scrobble_service.dart +++ b/lib/services/trakt/trakt_scrobble_service.dart @@ -55,7 +55,7 @@ class TraktScrobbleService { if (_isInitialized) return; _isInitialized = true; final settings = await SettingsService.getInstance(); - _isEnabled = settings.getEnableTraktScrobble(); + _isEnabled = settings.read(SettingsService.enableTraktScrobble); } Future setEnabled(bool enabled) async { diff --git a/lib/services/trakt/trakt_sync_service.dart b/lib/services/trakt/trakt_sync_service.dart index c9941cad..7015eccb 100644 --- a/lib/services/trakt/trakt_sync_service.dart +++ b/lib/services/trakt/trakt_sync_service.dart @@ -56,7 +56,7 @@ class TraktSyncService { _serverManager = serverManager; final settings = await SettingsService.getInstance(); - _isEnabled = settings.getEnableTraktWatchedSync(); + _isEnabled = settings.read(SettingsService.enableTraktWatchedSync); _subscription = WatchStateNotifier().stream.listen( _onWatchStateEvent, diff --git a/lib/utils/video_player_navigation.dart b/lib/utils/video_player_navigation.dart index d7a0d2df..4c06e3fb 100644 --- a/lib/utils/video_player_navigation.dart +++ b/lib/utils/video_player_navigation.dart @@ -70,7 +70,7 @@ Future navigateToVideoPlayer( try { final settingsService = await SettingsService.getInstance(); final seriesKey = metadata.grandparentRatingKey ?? metadata.ratingKey; - final savedPreference = settingsService.getMediaVersionPreference(seriesKey); + final savedPreference = settingsService.read(SettingsService.mediaVersionPreferences)[seriesKey]; if (savedPreference != null) { mediaIndex = savedPreference; // Pre-parsed playbackData was built with mediaIndex=0; invalidate if @@ -87,7 +87,7 @@ Future navigateToVideoPlayer( // Check if external player is enabled try { final settingsService = await SettingsService.getInstance(); - if (settingsService.getUseExternalPlayer()) { + if (settingsService.read(SettingsService.useExternalPlayer)) { bool launched = false; if (isOffline) { diff --git a/lib/watch_together/providers/watch_together_provider.dart b/lib/watch_together/providers/watch_together_provider.dart index b2d0c542..4b2b4224 100644 --- a/lib/watch_together/providers/watch_together_provider.dart +++ b/lib/watch_together/providers/watch_together_provider.dart @@ -210,7 +210,7 @@ class WatchTogetherProvider with ChangeNotifier { appLogger.d('WatchTogether: Creating session with control mode: $controlMode'); - final customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); + final customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl); _peerService = WatchTogetherPeerService(customBaseUrl: customRelayUrl); _setupPeerServiceListeners(); @@ -258,7 +258,7 @@ class WatchTogetherProvider with ChangeNotifier { appLogger.d('WatchTogether: Joining session: $sessionId'); - final customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); + final customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl); _peerService = WatchTogetherPeerService(customBaseUrl: customRelayUrl); _setupPeerServiceListeners(); @@ -312,7 +312,7 @@ class WatchTogetherProvider with ChangeNotifier { // Probe the relay with a lightweight peer service to check room occupancy, // then do a single createSession or joinSession. This avoids the crash-prone // join→teardown→create cycle on the provider. - final customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); + final customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl); final probe = WatchTogetherPeerService(customBaseUrl: customRelayUrl); bool shouldBeHost; try { diff --git a/lib/watch_together/screens/watch_together_screen.dart b/lib/watch_together/screens/watch_together_screen.dart index 1161d9a3..484f45e4 100644 --- a/lib/watch_together/screens/watch_together_screen.dart +++ b/lib/watch_together/screens/watch_together_screen.dart @@ -91,7 +91,7 @@ class _NotInSessionViewState extends State<_NotInSessionView> { @override void initState() { super.initState(); - _customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); + _customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl); _recentRooms = RecentRoomsService.getRecentRooms(); _checkHealth(); } diff --git a/lib/watch_together/services/recent_rooms_service.dart b/lib/watch_together/services/recent_rooms_service.dart index 3e7048a1..5a81715c 100644 --- a/lib/watch_together/services/recent_rooms_service.dart +++ b/lib/watch_together/services/recent_rooms_service.dart @@ -46,7 +46,7 @@ class RecentRoomsService { static const int _maxRooms = 20; static List getRecentRooms() { - final json = SettingsService.instanceOrNull?.getRecentRooms(); + final json = SettingsService.instanceOrNull?.read(SettingsService.recentRooms); if (json == null) return []; try { final list = jsonDecode(json) as List; @@ -61,7 +61,10 @@ class RecentRoomsService { static Future _save(List rooms) async { rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed)); if (rooms.length > _maxRooms) rooms.removeRange(_maxRooms, rooms.length); - await SettingsService.instanceOrNull?.setRecentRooms(jsonEncode(rooms.map((r) => r.toJson()).toList())); + await SettingsService.instanceOrNull?.write( + SettingsService.recentRooms, + jsonEncode(rooms.map((r) => r.toJson()).toList()), + ); } static Future addOrUpdateRoom(String code, {String? name, ControlMode? controlMode}) async { diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index a9e85259..7ec6fa9e 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -150,18 +150,18 @@ class _VideoSettingsSheetState extends State { final settings = await SettingsService.getInstance(); if (!mounted) return; setState(() { - _enableHDR = settings.getEnableHDR(); - _showPerformanceOverlay = settings.getShowPerformanceOverlay(); - _autoPlayNextEpisode = settings.getAutoPlayNextEpisode(); - _audioPassthrough = settings.getAudioPassthrough(); - _audioNormalization = settings.getAudioNormalization(); + _enableHDR = settings.read(SettingsService.enableHDR); + _showPerformanceOverlay = settings.read(SettingsService.showPerformanceOverlay); + _autoPlayNextEpisode = settings.read(SettingsService.autoPlayNextEpisode); + _audioPassthrough = settings.read(SettingsService.audioPassthrough); + _audioNormalization = settings.read(SettingsService.audioNormalization); }); } Future _toggleHDR() async { final newValue = !_enableHDR; final settings = await SettingsService.getInstance(); - await settings.setEnableHDR(newValue); + await settings.write(SettingsService.enableHDR, newValue); if (!mounted) return; setState(() { _enableHDR = newValue; @@ -173,7 +173,7 @@ class _VideoSettingsSheetState extends State { Future _togglePerformanceOverlay() async { final newValue = !_showPerformanceOverlay; final settings = await SettingsService.getInstance(); - await settings.setShowPerformanceOverlay(newValue); + await settings.write(SettingsService.showPerformanceOverlay, newValue); if (!mounted) return; setState(() { _showPerformanceOverlay = newValue; @@ -183,7 +183,7 @@ class _VideoSettingsSheetState extends State { Future _toggleAutoPlayNextEpisode() async { final newValue = !_autoPlayNextEpisode; final settings = await SettingsService.getInstance(); - await settings.setAutoPlayNextEpisode(newValue); + await settings.write(SettingsService.autoPlayNextEpisode, newValue); if (!mounted) return; setState(() { _autoPlayNextEpisode = newValue; @@ -193,7 +193,7 @@ class _VideoSettingsSheetState extends State { Future _toggleAudioPassthrough() async { final newValue = !_audioPassthrough; final settings = await SettingsService.getInstance(); - await settings.setAudioPassthrough(newValue); + await settings.write(SettingsService.audioPassthrough, newValue); if (!mounted) return; setState(() { _audioPassthrough = newValue; @@ -204,7 +204,7 @@ class _VideoSettingsSheetState extends State { Future _toggleAudioNormalization() async { final newValue = !_audioNormalization; final settings = await SettingsService.getInstance(); - await settings.setAudioNormalization(newValue); + await settings.write(SettingsService.audioNormalization, newValue); if (!mounted) return; setState(() { _audioNormalization = newValue; @@ -256,9 +256,9 @@ class _VideoSettingsSheetState extends State { onOffsetChanged: (offset) async { final settings = await SettingsService.getInstance(); if (isSubtitle) { - await settings.setSubtitleSyncOffset(offset); + await settings.write(SettingsService.subtitleSyncOffset, offset); } else { - await settings.setAudioSyncOffset(offset); + await settings.write(SettingsService.audioSyncOffset, offset); } widget.onSyncOffsetChanged?.call(propertyName, offset); }, @@ -562,7 +562,7 @@ class _VideoSettingsSheetState extends State { widget.player.setRate(speed); // Save as default playback speed final settings = await SettingsService.getInstance(); - await settings.setDefaultPlaybackSpeed(speed); + await settings.write(SettingsService.defaultPlaybackSpeed, speed); if (context.mounted) { OverlaySheetController.of(context).close(); // Close sheet after selection } diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index d1c5f10a..05da552c 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -647,18 +647,18 @@ class _PlexVideoControlsState extends State with WindowListen final settingsService = await SettingsService.getInstance(); if (mounted) { setState(() { - _seekTimeSmall = settingsService.getSeekTimeSmall(); - _rewindOnResume = settingsService.getRewindOnResume(); - _audioSyncOffset = settingsService.getAudioSyncOffset(); - _subtitleSyncOffset = settingsService.getSubtitleSyncOffset(); - _isRotationLocked = settingsService.getRotationLocked(); - _autoSkipIntro = settingsService.getAutoSkipIntro(); - _autoSkipCredits = settingsService.getAutoSkipCredits(); - _autoSkipDelay = settingsService.getAutoSkipDelay(); - _videoPlayerNavigationEnabled = settingsService.getVideoPlayerNavigationEnabled(); - _showPerformanceOverlay = settingsService.getShowPerformanceOverlay(); - _autoHidePerformanceOverlay = settingsService.getAutoHidePerformanceOverlay(); - _clickVideoTogglesPlayback = settingsService.getClickVideoTogglesPlayback(); + _seekTimeSmall = settingsService.read(SettingsService.seekTimeSmall); + _rewindOnResume = settingsService.read(SettingsService.rewindOnResume); + _audioSyncOffset = settingsService.read(SettingsService.audioSyncOffset); + _subtitleSyncOffset = settingsService.read(SettingsService.subtitleSyncOffset); + _isRotationLocked = settingsService.read(SettingsService.rotationLocked); + _autoSkipIntro = settingsService.read(SettingsService.autoSkipIntro); + _autoSkipCredits = settingsService.read(SettingsService.autoSkipCredits); + _autoSkipDelay = settingsService.read(SettingsService.autoSkipDelay); + _videoPlayerNavigationEnabled = settingsService.read(SettingsService.videoPlayerNavigationEnabled); + _showPerformanceOverlay = settingsService.read(SettingsService.showPerformanceOverlay); + _autoHidePerformanceOverlay = settingsService.read(SettingsService.autoHidePerformanceOverlay); + _clickVideoTogglesPlayback = settingsService.read(SettingsService.clickVideoTogglesPlayback); }); // Focus play/pause if navigation is now enabled and controls are visible @@ -940,7 +940,7 @@ class _PlexVideoControlsState extends State with WindowListen final maxVol = _keyboardService!.maxVolume.toDouble(); final newVolume = (volume - delta / 20).clamp(0.0, maxVol); widget.player.setVolume(newVolume); - unawaited(SettingsService.getInstance().then((s) => s.setVolume(newVolume))); + unawaited(SettingsService.getInstance().then((s) => s.write(SettingsService.volume, newVolume))); _showControlsFromPointerActivity(); } } @@ -990,7 +990,7 @@ class _PlexVideoControlsState extends State with WindowListen // Save to settings final settingsService = await SettingsService.getInstance(); - await settingsService.setRotationLocked(_isRotationLocked); + await settingsService.write(SettingsService.rotationLocked, _isRotationLocked); if (_isRotationLocked) { // Locked: Allow landscape orientations only @@ -1085,8 +1085,8 @@ class _PlexVideoControlsState extends State with WindowListen appLogger.d('_loadPlaybackExtras: got client with serverId=${client.serverId}'); final settings = await SettingsService.getInstance(); - final introPattern = settings.getIntroPattern(); - final creditsPattern = settings.getCreditsPattern(); + final introPattern = settings.read(SettingsService.introPattern); + final creditsPattern = settings.read(SettingsService.creditsPattern); final extras = await client.getPlaybackExtras( widget.metadata.ratingKey, introPattern: introPattern, @@ -1172,8 +1172,8 @@ class _PlexVideoControlsState extends State with WindowListen return PlaybackExtras.withChapterFallback( chapters: chapters, markers: markers, - introPatternStr: settings.getIntroPattern(), - creditsPatternStr: settings.getCreditsPattern(), + introPatternStr: settings.read(SettingsService.introPattern), + creditsPatternStr: settings.read(SettingsService.creditsPattern), ); } @@ -2592,7 +2592,10 @@ class _PlexVideoControlsState extends State with WindowListen if (isVersionChange) { final settingsService = await SettingsService.getInstance(); final seriesKey = widget.metadata.grandparentRatingKey ?? widget.metadata.ratingKey; - await settingsService.setMediaVersionPreference(seriesKey, effectiveMediaIndex); + await settingsService.write(SettingsService.mediaVersionPreferences, { + ...settingsService.read(SettingsService.mediaVersionPreferences), + seriesKey: effectiveMediaIndex, + }); } // Preserve session identifiers across the reload so Plex reuses the diff --git a/lib/widgets/video_controls/widgets/volume_control.dart b/lib/widgets/video_controls/widgets/volume_control.dart index f3b0b041..efd4bb5a 100644 --- a/lib/widgets/video_controls/widgets/volume_control.dart +++ b/lib/widgets/video_controls/widgets/volume_control.dart @@ -67,7 +67,7 @@ class _VolumeControlState extends State { final settings = await SettingsService.getInstance(); if (mounted) { setState(() { - _maxVolume = settings.getMaxVolume(); + _maxVolume = settings.read(SettingsService.maxVolume); }); } } @@ -89,7 +89,7 @@ class _VolumeControlState extends State { final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble()); widget.player.setVolume(newVolume); final settings = await SettingsService.getInstance(); - await settings.setVolume(newVolume); + await settings.write(SettingsService.volume, newVolume); } KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { @@ -170,7 +170,7 @@ class _VolumeControlState extends State { final newVolume = isMuted ? 100.0 : 0.0; widget.player.setVolume(newVolume); final settings = await SettingsService.getInstance(); - await settings.setVolume(newVolume); + await settings.write(SettingsService.volume, newVolume); }, ), ); @@ -261,7 +261,7 @@ class _VolumeControlState extends State { }, onChangeEnd: (value) async { final settings = await SettingsService.getInstance(); - await settings.setVolume(value); + await settings.write(SettingsService.volume, value); }, activeColor: Colors.white, inactiveColor: Colors.white.withValues(alpha: 0.3), diff --git a/pubspec.yaml b/pubspec.yaml index 4c42758e..b3969126 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: sdk: flutter intl: ^0.20.2 json_annotation: ^4.9.0 - shared_preferences: ^2.2.2 + shared_preferences: ^2.5.4 cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 http: ^1.2.0