refactor(settings): typed Pref<T> declarations

This commit is contained in:
edde746
2026-04-25 10:52:34 +02:00
parent e4e8299981
commit 0531c33b0c
42 changed files with 1102 additions and 1971 deletions
+5 -5
View File
@@ -135,7 +135,7 @@ Future<void> main() async {
Future<void> _bootstrapApp() async { Future<void> _bootstrapApp() async {
// Initialize settings first to get saved locale // Initialize settings first to get saved locale
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final savedLocale = settings.getAppLocale(); final savedLocale = settings.read(SettingsService.appLocale);
// Initialize localization with saved locale // Initialize localization with saved locale
LocaleSettings.setLocale(savedLocale); LocaleSettings.setLocale(savedLocale);
@@ -162,7 +162,7 @@ Future<void> _bootstrapApp() async {
// Initialize TV detection (Android leanback or Apple TV) and PiP on Android. // Initialize TV detection (Android leanback or Apple TV) and PiP on Android.
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
futures.add(TvDetectionService.getInstance(forceTv: settings.getForceTvMode())); futures.add(TvDetectionService.getInstance(forceTv: settings.read(SettingsService.forceTvMode)));
} }
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Initialize PiP service to listen for PiP state changes (Android only). // Initialize PiP service to listen for PiP state changes (Android only).
@@ -188,7 +188,7 @@ Future<void> _bootstrapApp() async {
} }
// Initialize logger level based on debug setting // Initialize logger level based on debug setting
final debugEnabled = settings.getEnableDebugLogging(); final debugEnabled = settings.read(SettingsService.enableDebugLogging);
setLoggerLevel(debugEnabled); setLoggerLevel(debugEnabled);
// Log app version and git commit at startup // Log app version and git commit at startup
@@ -248,7 +248,7 @@ Breadcrumb? _beforeBreadcrumb(Breadcrumb? breadcrumb, Hint _) {
FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) { FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
// Drop event if user opted out of crash reporting // Drop event if user opted out of crash reporting
final instance = SettingsService.instanceOrNull; final instance = SettingsService.instanceOrNull;
if (instance != null && !instance.getCrashReporting()) return null; if (instance != null && !instance.read(SettingsService.crashReporting)) return null;
// Drop unactionable errors // Drop unactionable errors
var exceptions = event.exceptions; var exceptions = event.exceptions;
@@ -530,7 +530,7 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
await downloadProvider.refreshMetadataFromCache(); await downloadProvider.refreshMetadataFromCache();
final activeKey = VideoPlayerScreenState.activeRatingKey; final activeKey = VideoPlayerScreenState.activeRatingKey;
final settings = SettingsService.instanceOrNull; final settings = SettingsService.instanceOrNull;
if (settings != null && settings.getAutoRemoveWatchedDownloads()) { if (settings != null && settings.read(SettingsService.autoRemoveWatchedDownloads)) {
final deleted = await downloadProvider.autoDeleteWatchedDownloads(activeRatingKey: activeKey); final deleted = await downloadProvider.autoDeleteWatchedDownloads(activeRatingKey: activeKey);
if (deleted.isNotEmpty) { if (deleted.isNotEmpty) {
final msg = deleted.length == 1 final msg = deleted.length == 1
+1 -1
View File
@@ -185,7 +185,7 @@ class OfflineWatchProvider extends ChangeNotifier {
/// Auto-delete a download if the auto-remove setting is enabled. /// Auto-delete a download if the auto-remove setting is enabled.
void _autoDeleteIfWatched(String serverId, String ratingKey) { void _autoDeleteIfWatched(String serverId, String ratingKey) {
final settings = SettingsService.instanceOrNull; final settings = SettingsService.instanceOrNull;
if (settings == null || !settings.getAutoRemoveWatchedDownloads()) return; if (settings == null || !settings.read(SettingsService.autoRemoveWatchedDownloads)) return;
final globalKey = buildGlobalKey(serverId, ratingKey); final globalKey = buildGlobalKey(serverId, ratingKey);
final meta = _downloadProvider.getMetadata(globalKey); final meta = _downloadProvider.getMetadata(globalKey);
+44 -192
View File
@@ -1,229 +1,81 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/transcode_quality_preset.dart'; import '../models/transcode_quality_preset.dart';
import '../services/base_shared_preferences_service.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
class SettingsProvider extends ChangeNotifier { class SettingsProvider extends ChangeNotifier {
SettingsService? _settingsService; 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; bool _isInitialized = false;
Future<void>? _initFuture; Future<void>? _initFuture;
SettingsProvider() { SettingsProvider() {
// Start initialization eagerly to reduce race conditions
_initFuture = _initializeSettings(); _initFuture = _initializeSettings();
} }
/// Ensures the provider is initialized. Call this before accessing settings
/// in contexts where you need the actual persisted values.
Future<void> ensureInitialized() => _initFuture ?? _initializeSettings(); Future<void> ensureInitialized() => _initFuture ?? _initializeSettings();
/// Whether the provider has finished loading persisted settings.
/// Synchronous getters below return defaults until this is true.
bool get isReady => _isInitialized; bool get isReady => _isInitialized;
bool get isInitialized => _isInitialized;
/// Future that completes once persisted settings have been loaded.
Future<void> get ready => _initFuture ?? Future.value(); Future<void> get ready => _initFuture ?? Future.value();
Future<void> _initializeSettings() async { Future<void> _initializeSettings() async {
if (_isInitialized) return; if (_isInitialized) return;
_settingsService = await SettingsService.getInstance(); _settingsService = await SettingsService.getInstance();
_readAllFromService();
_isInitialized = true; _isInitialized = true;
notifyListeners(); notifyListeners();
} }
void _readAllFromService() { /// Re-read settings after an external mutation (import, reset). The provider
final s = _settingsService!; /// no longer caches values, so this just re-fetches the service instance and
_libraryDensity = s.getLibraryDensity(); /// notifies listeners.
_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.
Future<void> reload() async { Future<void> reload() async {
_settingsService = await SettingsService.getInstance(); _settingsService = await SettingsService.getInstance();
_readAllFromService();
_isInitialized = true; _isInitialized = true;
notifyListeners(); notifyListeners();
} }
/// Whether the provider has completed initialization T _read<T>(Pref<T> pref, T fallback) => _isInitialized ? _settingsService!.read(pref) : fallback;
bool get isInitialized => _isInitialized;
int get libraryDensity => _libraryDensity; Future<void> _set<T>(Pref<T> pref, T value) async {
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<void> _updateSetting<T>({
required T current,
required T value,
required void Function(T) setLocal,
required Future<void> Function(T) persist,
}) async {
if (!_isInitialized) await _initializeSettings(); if (!_isInitialized) await _initializeSettings();
if (current != value) { if (_settingsService!.read(pref) == value) return;
setLocal(value); await _settingsService!.write(pref, value);
await persist(value); notifyListeners();
notifyListeners();
}
} }
Future<void> setLibraryDensity(int density) => _updateSetting( int get libraryDensity => _read(SettingsService.libraryDensity, LibraryDensity.defaultValue);
current: _libraryDensity, ViewMode get viewMode => _read(SettingsService.viewMode, ViewMode.grid);
value: density.clamp(LibraryDensity.min, LibraryDensity.max), EpisodePosterMode get episodePosterMode =>
setLocal: (v) => _libraryDensity = v, _read(SettingsService.episodePosterMode, EpisodePosterMode.episodeThumbnail);
persist: _settingsService!.setLibraryDensity, 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<void> setViewMode(ViewMode mode) => _updateSetting( Future<void> setLibraryDensity(int density) =>
current: _viewMode, _set(SettingsService.libraryDensity, density.clamp(LibraryDensity.min, LibraryDensity.max));
value: mode, Future<void> setViewMode(ViewMode mode) => _set(SettingsService.viewMode, mode);
setLocal: (v) => _viewMode = v, Future<void> setEpisodePosterMode(EpisodePosterMode mode) => _set(SettingsService.episodePosterMode, mode);
persist: _settingsService!.setViewMode, Future<void> setShowHeroSection(bool value) => _set(SettingsService.showHeroSection, value);
); Future<void> setUseGlobalHubs(bool value) => _set(SettingsService.useGlobalHubs, value);
Future<void> setShowServerNameOnHubs(bool value) => _set(SettingsService.showServerNameOnHubs, value);
Future<void> setEpisodePosterMode(EpisodePosterMode mode) => _updateSetting( Future<void> setGroupLibrariesByServer(bool value) => _set(SettingsService.groupLibrariesByServer, value);
current: _episodePosterMode, Future<void> setAlwaysKeepSidebarOpen(bool value) => _set(SettingsService.alwaysKeepSidebarOpen, value);
value: mode, Future<void> setShowUnwatchedCount(bool value) => _set(SettingsService.showUnwatchedCount, value);
setLocal: (v) => _episodePosterMode = v, Future<void> setShowEpisodeNumberOnCards(bool value) => _set(SettingsService.showEpisodeNumberOnCards, value);
persist: _settingsService!.setEpisodePosterMode, Future<void> setHideSpoilers(bool value) => _set(SettingsService.hideSpoilers, value);
); Future<void> setShowNavBarLabels(bool value) => _set(SettingsService.showNavBarLabels, value);
Future<void> setLiveTvDefaultFavorites(bool value) => _set(SettingsService.liveTvDefaultFavorites, value);
Future<void> setShowHeroSection(bool value) => _updateSetting( Future<void> setAutoHidePerformanceOverlay(bool value) => _set(SettingsService.autoHidePerformanceOverlay, value);
current: _showHeroSection, Future<void> setDefaultQualityPreset(TranscodeQualityPreset preset) =>
value: value, _set(SettingsService.defaultQualityPreset, preset.storageKey);
setLocal: (v) => _showHeroSection = v,
persist: _settingsService!.setShowHeroSection,
);
Future<void> setUseGlobalHubs(bool value) => _updateSetting(
current: _useGlobalHubs,
value: value,
setLocal: (v) => _useGlobalHubs = v,
persist: _settingsService!.setUseGlobalHubs,
);
Future<void> setShowServerNameOnHubs(bool value) => _updateSetting(
current: _showServerNameOnHubs,
value: value,
setLocal: (v) => _showServerNameOnHubs = v,
persist: _settingsService!.setShowServerNameOnHubs,
);
Future<void> setGroupLibrariesByServer(bool value) => _updateSetting(
current: _groupLibrariesByServer,
value: value,
setLocal: (v) => _groupLibrariesByServer = v,
persist: _settingsService!.setGroupLibrariesByServer,
);
Future<void> setAlwaysKeepSidebarOpen(bool value) => _updateSetting(
current: _alwaysKeepSidebarOpen,
value: value,
setLocal: (v) => _alwaysKeepSidebarOpen = v,
persist: _settingsService!.setAlwaysKeepSidebarOpen,
);
Future<void> setShowUnwatchedCount(bool value) => _updateSetting(
current: _showUnwatchedCount,
value: value,
setLocal: (v) => _showUnwatchedCount = v,
persist: _settingsService!.setShowUnwatchedCount,
);
Future<void> setShowEpisodeNumberOnCards(bool value) => _updateSetting(
current: _showEpisodeNumberOnCards,
value: value,
setLocal: (v) => _showEpisodeNumberOnCards = v,
persist: _settingsService!.setShowEpisodeNumberOnCards,
);
Future<void> setHideSpoilers(bool value) => _updateSetting(
current: _hideSpoilers,
value: value,
setLocal: (v) => _hideSpoilers = v,
persist: _settingsService!.setHideSpoilers,
);
Future<void> setShowNavBarLabels(bool value) => _updateSetting(
current: _showNavBarLabels,
value: value,
setLocal: (v) => _showNavBarLabels = v,
persist: _settingsService!.setShowNavBarLabels,
);
Future<void> setLiveTvDefaultFavorites(bool value) => _updateSetting(
current: _liveTvDefaultFavorites,
value: value,
setLocal: (v) => _liveTvDefaultFavorites = v,
persist: _settingsService!.setLiveTvDefaultFavorites,
);
Future<void> setAutoHidePerformanceOverlay(bool value) => _updateSetting(
current: _autoHidePerformanceOverlay,
value: value,
setLocal: (v) => _autoHidePerformanceOverlay = v,
persist: _settingsService!.setAutoHidePerformanceOverlay,
);
Future<void> setDefaultQualityPreset(TranscodeQualityPreset preset) => _updateSetting(
current: _defaultQualityPreset,
value: preset,
setLocal: (v) => _defaultQualityPreset = v,
persist: (v) => _settingsService!.setDefaultQualityPreset(v.storageKey),
);
} }
+5 -5
View File
@@ -23,10 +23,10 @@ class ShaderProvider extends ChangeNotifier {
_settingsService = await SettingsService.getInstance(); _settingsService = await SettingsService.getInstance();
// Load custom presets from storage // Load custom presets from storage
final customData = _settingsService.getCustomShaderPresets(); final customData = _settingsService.read(SettingsService.customShaderPresets);
_customPresets = customData.map((json) => ShaderPreset.fromJson(json)).toList(); _customPresets = customData.map((json) => ShaderPreset.fromJson(json)).toList();
final presetId = _settingsService.getGlobalShaderPreset(); final presetId = _settingsService.read(SettingsService.globalShaderPreset);
_savedPreset = findPresetById(presetId) ?? ShaderPreset.none; _savedPreset = findPresetById(presetId) ?? ShaderPreset.none;
_currentPreset = _savedPreset; _currentPreset = _savedPreset;
@@ -62,7 +62,7 @@ class ShaderProvider extends ChangeNotifier {
Future<void> setPreset(ShaderPreset preset) async { Future<void> setPreset(ShaderPreset preset) async {
_savedPreset = preset; _savedPreset = preset;
_currentPreset = preset; _currentPreset = preset;
await _settingsService.setGlobalShaderPreset(preset.id); await _settingsService.write(SettingsService.globalShaderPreset, preset.id);
notifyListeners(); notifyListeners();
} }
@@ -100,7 +100,7 @@ class ShaderProvider extends ChangeNotifier {
if (_currentPreset.id == preset.id || _savedPreset.id == preset.id) { if (_currentPreset.id == preset.id || _savedPreset.id == preset.id) {
_savedPreset = ShaderPreset.none; _savedPreset = ShaderPreset.none;
_currentPreset = ShaderPreset.none; _currentPreset = ShaderPreset.none;
await _settingsService.setGlobalShaderPreset(ShaderPreset.none.id); await _settingsService.write(SettingsService.globalShaderPreset, ShaderPreset.none.id);
} }
notifyListeners(); notifyListeners();
@@ -108,7 +108,7 @@ class ShaderProvider extends ChangeNotifier {
Future<void> _saveCustomPresets() async { Future<void> _saveCustomPresets() async {
final data = _customPresets.map((p) => p.toJson()).toList(); final data = _customPresets.map((p) => p.toJson()).toList();
await _settingsService.setCustomShaderPresets(data); await _settingsService.write(SettingsService.customShaderPresets, data);
} }
/// Reset to default (no shaders) /// Reset to default (no shaders)
+3 -3
View File
@@ -33,7 +33,7 @@ class ThemeProvider extends ChangeNotifier {
Future<void> _initializeSettings() async { Future<void> _initializeSettings() async {
_settingsService = await settings.SettingsService.getInstance(); _settingsService = await settings.SettingsService.getInstance();
_themeMode = _settingsService.getThemeMode(); _themeMode = _settingsService.read(settings.SettingsService.themeMode);
_updateSplashTheme(_themeMode); _updateSplashTheme(_themeMode);
notifyListeners(); notifyListeners();
} }
@@ -79,7 +79,7 @@ class ThemeProvider extends ChangeNotifier {
Future<void> setThemeMode(settings.ThemeMode mode) async { Future<void> setThemeMode(settings.ThemeMode mode) async {
if (_themeMode != mode) { if (_themeMode != mode) {
_themeMode = mode; _themeMode = mode;
await _settingsService.setThemeMode(mode); await _settingsService.write(settings.SettingsService.themeMode, mode);
_updateSplashTheme(mode); _updateSplashTheme(mode);
notifyListeners(); notifyListeners();
} }
@@ -89,7 +89,7 @@ class ThemeProvider extends ChangeNotifier {
/// resets that change persisted settings outside this provider. /// resets that change persisted settings outside this provider.
Future<void> reload() async { Future<void> reload() async {
_settingsService = await settings.SettingsService.getInstance(); _settingsService = await settings.SettingsService.getInstance();
final mode = _settingsService.getThemeMode(); final mode = _settingsService.read(settings.SettingsService.themeMode);
_themeMode = mode; _themeMode = mode;
_updateSplashTheme(mode); _updateSplashTheme(mode);
notifyListeners(); notifyListeners();
+7 -6
View File
@@ -199,7 +199,8 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
final needsInitial = userProfileProvider.needsInitialProfileSelection; final needsInitial = userProfileProvider.needsInitialProfileSelection;
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
if (!mounted) return; if (!mounted) return;
final requireOnOpen = settingsService.getRequireProfileSelectionOnOpen() && userProfileProvider.hasMultipleUsers; final requireOnOpen =
settingsService.read(SettingsService.requireProfileSelectionOnOpen) && userProfileProvider.hasMultipleUsers;
if (!needsInitial && !requireOnOpen) return; if (!needsInitial && !requireOnOpen) return;
@@ -214,7 +215,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
if (!mounted) return; if (!mounted) return;
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
if (!settingsService.getAutoCheckUpdatesOnStartup()) return; if (!settingsService.read(SettingsService.autoCheckUpdatesOnStartup)) return;
// Native updater (Sparkle/WinSparkle) handles everything — skip Flutter dialog // Native updater (Sparkle/WinSparkle) handles everything — skip Flutter dialog
if (UpdateService.useNativeUpdater) { if (UpdateService.useNativeUpdater) {
@@ -449,7 +450,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
Future<void> _autoStartCompanionRemoteServer(CompanionRemoteProvider companionRemote) async { Future<void> _autoStartCompanionRemoteServer(CompanionRemoteProvider companionRemote) async {
try { try {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (!settings.getEnableCompanionRemoteServer()) return; if (!settings.read(SettingsService.enableCompanionRemoteServer)) return;
if (!mounted) return; if (!mounted) return;
final home = context.read<UserProfileProvider>().home; final home = context.read<UserProfileProvider>().home;
@@ -510,7 +511,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
Future<void> _showProfileSelectionOnResume() async { Future<void> _showProfileSelectionOnResume() async {
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
if (!settingsService.getRequireProfileSelectionOnOpen()) return; if (!settingsService.read(SettingsService.requireProfileSelectionOnOpen)) return;
if (!mounted) return; if (!mounted) return;
final userProfileProvider = context.read<UserProfileProvider>(); final userProfileProvider = context.read<UserProfileProvider>();
@@ -682,7 +683,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
return handleBackKeyAction(event, () async { return handleBackKeyAction(event, () async {
if (PlatformDetector.isTV()) { if (PlatformDetector.isTV()) {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (settings.getConfirmExitOnBack() && mounted) { if (settings.read(SettingsService.confirmExitOnBack) && mounted) {
final result = await showConfirmDialogWithCheckbox( final result = await showConfirmDialogWithCheckbox(
context, context,
title: t.common.exitConfirmTitle, title: t.common.exitConfirmTitle,
@@ -691,7 +692,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
checkboxLabel: t.common.dontAskAgain, checkboxLabel: t.common.dontAskAgain,
); );
if (result.checked) { if (result.checked) {
await settings.setConfirmExitOnBack(false); await settings.write(SettingsService.confirmExitOnBack, false);
} }
if (!result.confirmed) return; if (!result.confirmed) return;
} }
@@ -26,9 +26,6 @@ class AppearanceSettingsScreen extends StatefulWidget {
class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> { class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
late settings.SettingsService _settingsService; late settings.SettingsService _settingsService;
bool _isLoading = true; bool _isLoading = true;
bool _requireProfileSelectionOnOpen = false;
bool _confirmExitOnBack = true;
bool _forceTvMode = false;
@override @override
void initState() { void initState() {
@@ -39,12 +36,7 @@ class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
_settingsService = await settings.SettingsService.getInstance(); _settingsService = await settings.SettingsService.getInstance();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() => _isLoading = false);
_requireProfileSelectionOnOpen = _settingsService.getRequireProfileSelectionOnOpen();
_confirmExitOnBack = _settingsService.getConfirmExitOnBack();
_forceTvMode = _settingsService.getForceTvMode();
_isLoading = false;
});
} }
@override @override
@@ -135,7 +127,7 @@ class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
currentValue: LocaleSettings.currentLocale, currentValue: LocaleSettings.currentLocale,
); );
if (value != null) { if (value != null) {
await _settingsService.setAppLocale(value); await _settingsService.write(settings.SettingsService.appLocale, value);
LocaleSettings.setLocale(value); LocaleSettings.setLocale(value);
_restartApp(); _restartApp();
} }
@@ -333,45 +325,55 @@ class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
return Consumer<UserProfileProvider>( return Consumer<UserProfileProvider>(
builder: (context, userProfileProvider, child) { builder: (context, userProfileProvider, child) {
if (!userProfileProvider.hasMultipleUsers) return const SizedBox.shrink(); if (!userProfileProvider.hasMultipleUsers) return const SizedBox.shrink();
return SwitchListTile( return _buildListenableSwitch(
secondary: const AppIcon(Symbols.person_rounded, fill: 1), icon: Symbols.person_rounded,
title: Text(t.settings.requireProfileSelectionOnOpen), title: t.settings.requireProfileSelectionOnOpen,
subtitle: Text(t.settings.requireProfileSelectionOnOpenDescription), subtitle: t.settings.requireProfileSelectionOnOpenDescription,
value: _requireProfileSelectionOnOpen, pref: settings.SettingsService.requireProfileSelectionOnOpen,
onChanged: (value) async {
setState(() => _requireProfileSelectionOnOpen = value);
await _settingsService.setRequireProfileSelectionOnOpen(value);
},
); );
}, },
); );
} }
Widget _buildConfirmExitOnBack() { Widget _buildConfirmExitOnBack() => _buildListenableSwitch(
return SwitchListTile( icon: Symbols.exit_to_app_rounded,
secondary: const AppIcon(Symbols.exit_to_app_rounded, fill: 1), title: t.settings.confirmExitOnBack,
title: Text(t.settings.confirmExitOnBack), subtitle: t.settings.confirmExitOnBackDescription,
subtitle: Text(t.settings.confirmExitOnBackDescription), pref: settings.SettingsService.confirmExitOnBack,
value: _confirmExitOnBack, );
onChanged: (value) async {
setState(() => _confirmExitOnBack = value);
await _settingsService.setConfirmExitOnBack(value);
},
);
}
Widget _buildForceTvMode() { Widget _buildForceTvMode() => _buildListenableSwitch(
return SwitchListTile( icon: Symbols.tv_rounded,
secondary: const AppIcon(Symbols.tv_rounded, fill: 1), title: t.settings.forceTvMode,
title: Text(t.settings.forceTvMode), subtitle: t.settings.forceTvModeDescription,
subtitle: Text(t.settings.forceTvModeDescription), pref: settings.SettingsService.forceTvMode,
value: _forceTvMode, onAfterWrite: (value) {
onChanged: (value) async { TvDetectionService.setForceTVSync(value);
setState(() => _forceTvMode = value); if (mounted) _restartApp();
await _settingsService.setForceTvMode(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<bool> pref,
void Function(bool)? onAfterWrite,
}) {
return ValueListenableBuilder<bool>(
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);
},
),
); );
} }
@@ -37,9 +37,9 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_useExternalPlayer = _settingsService.getUseExternalPlayer(); _useExternalPlayer = _settingsService.read(SettingsService.useExternalPlayer);
_selectedPlayer = _settingsService.getSelectedExternalPlayer(); _selectedPlayer = _settingsService.read(SettingsService.selectedExternalPlayer);
_customPlayers = _settingsService.getCustomExternalPlayers(); _customPlayers = _settingsService.read(SettingsService.customExternalPlayers);
_isLoading = false; _isLoading = false;
}); });
} }
@@ -67,7 +67,7 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
value: _useExternalPlayer, value: _useExternalPlayer,
onChanged: (value) async { onChanged: (value) async {
setState(() => _useExternalPlayer = value); setState(() => _useExternalPlayer = value);
await _settingsService.setUseExternalPlayer(value); await _settingsService.write(SettingsService.useExternalPlayer, value);
}, },
), ),
if (_useExternalPlayer) ...[ if (_useExternalPlayer) ...[
@@ -132,7 +132,7 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
), ),
onTap: () async { onTap: () async {
setState(() => _selectedPlayer = player); setState(() => _selectedPlayer = player);
await _settingsService.setSelectedExternalPlayer(player); await _settingsService.write(SettingsService.selectedExternalPlayer, player);
}, },
); );
} }
@@ -142,7 +142,7 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_customPlayers.removeWhere((p) => p.id == player.id); _customPlayers.removeWhere((p) => p.id == player.id);
_selectedPlayer = _settingsService.getSelectedExternalPlayer(); _selectedPlayer = _settingsService.read(SettingsService.selectedExternalPlayer);
}); });
} }
@@ -255,7 +255,10 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
type: selectedType, type: selectedType,
); );
await _settingsService.addCustomExternalPlayer(newPlayer); await _settingsService.write(SettingsService.customExternalPlayers, [
..._settingsService.read(SettingsService.customExternalPlayers),
newPlayer,
]);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_customPlayers.add(newPlayer); _customPlayers.add(newPlayer);
+6 -6
View File
@@ -47,14 +47,14 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_textController.text = _settingsService.getMpvConfigText(); _textController.text = _settingsService.read(SettingsService.mpvConfigText);
_presets = _settingsService.getMpvPresets(); _presets = _settingsService.read(SettingsService.mpvPresets);
_isLoading = false; _isLoading = false;
}); });
} }
Future<void> _saveText() async { Future<void> _saveText() async {
await _settingsService.setMpvConfigText(_textController.text); await _settingsService.write(SettingsService.mpvConfigText, _textController.text);
} }
Future<void> _showSavePresetDialog() async { Future<void> _showSavePresetDialog() async {
@@ -71,7 +71,7 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
await _settingsService.saveMpvPreset(name.trim(), _textController.text); await _settingsService.saveMpvPreset(name.trim(), _textController.text);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_presets = _settingsService.getMpvPresets(); _presets = _settingsService.read(SettingsService.mpvPresets);
}); });
if (mounted) { if (mounted) {
@@ -84,7 +84,7 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
await _settingsService.loadMpvPreset(preset.name); await _settingsService.loadMpvPreset(preset.name);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_textController.text = _settingsService.getMpvConfigText(); _textController.text = _settingsService.read(SettingsService.mpvConfigText);
}); });
if (mounted) { if (mounted) {
@@ -103,7 +103,7 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
await _settingsService.deleteMpvPreset(preset.name); await _settingsService.deleteMpvPreset(preset.name);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_presets = _settingsService.getMpvPresets(); _presets = _settingsService.read(SettingsService.mpvPresets);
}); });
if (mounted) { if (mounted) {
@@ -73,32 +73,34 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_enableHardwareDecoding = _settingsService.getEnableHardwareDecoding(); _enableHardwareDecoding = _settingsService.read(settings.SettingsService.enableHardwareDecoding);
_bufferSize = _settingsService.getBufferSize(); _bufferSize = _settingsService.read(settings.SettingsService.bufferSize);
_seekTimeSmall = _settingsService.getSeekTimeSmall(); _seekTimeSmall = _settingsService.read(settings.SettingsService.seekTimeSmall);
_seekTimeLarge = _settingsService.getSeekTimeLarge(); _seekTimeLarge = _settingsService.read(settings.SettingsService.seekTimeLarge);
_rewindOnResume = _settingsService.getRewindOnResume(); _rewindOnResume = _settingsService.read(settings.SettingsService.rewindOnResume);
_sleepTimerDuration = _settingsService.getSleepTimerDuration(); _sleepTimerDuration = _settingsService.read(settings.SettingsService.sleepTimerDuration);
_rememberTrackSelections = _settingsService.getRememberTrackSelections(); _rememberTrackSelections = _settingsService.read(settings.SettingsService.rememberTrackSelections);
_clickVideoTogglesPlayback = _settingsService.getClickVideoTogglesPlayback(); _clickVideoTogglesPlayback = _settingsService.read(settings.SettingsService.clickVideoTogglesPlayback);
_autoSkipIntro = _settingsService.getAutoSkipIntro(); _autoSkipIntro = _settingsService.read(settings.SettingsService.autoSkipIntro);
_autoSkipCredits = _settingsService.getAutoSkipCredits(); _autoSkipCredits = _settingsService.read(settings.SettingsService.autoSkipCredits);
_autoSkipDelay = _settingsService.getAutoSkipDelay(); _autoSkipDelay = _settingsService.read(settings.SettingsService.autoSkipDelay);
_introPattern = _settingsService.getIntroPattern(); _introPattern = _settingsService.read(settings.SettingsService.introPattern);
_creditsPattern = _settingsService.getCreditsPattern(); _creditsPattern = _settingsService.read(settings.SettingsService.creditsPattern);
_maxVolume = _settingsService.getMaxVolume(); _maxVolume = _settingsService.read(settings.SettingsService.maxVolume);
_enableDiscordRPC = _settingsService.getEnableDiscordRPC(); _enableDiscordRPC = _settingsService.read(settings.SettingsService.enableDiscordRPC);
_enableCompanionRemoteServer = _settingsService.getEnableCompanionRemoteServer(); _enableCompanionRemoteServer = _settingsService.read(settings.SettingsService.enableCompanionRemoteServer);
_autoPip = _settingsService.getAutoPip(); _autoPip = _settingsService.read(settings.SettingsService.autoPip);
_matchContentFrameRate = _settingsService.getMatchContentFrameRate(); _matchContentFrameRate = _settingsService.read(settings.SettingsService.matchContentFrameRate);
_matchRefreshRate = _settingsService.getMatchRefreshRate(); _matchRefreshRate = _settingsService.read(settings.SettingsService.matchRefreshRate);
_matchDynamicRange = _settingsService.getMatchDynamicRange(); _matchDynamicRange = _settingsService.read(settings.SettingsService.matchDynamicRange);
_displaySwitchDelay = _settingsService.getDisplaySwitchDelay(); _displaySwitchDelay = _settingsService.read(settings.SettingsService.displaySwitchDelay);
_tunneledPlayback = _settingsService.getTunneledPlayback(); _tunneledPlayback = _settingsService.read(settings.SettingsService.tunneledPlayback);
_useExoPlayer = _settingsService.getUseExoPlayer(); _useExoPlayer = _settingsService.read(settings.SettingsService.useExoPlayer);
_useExternalPlayer = _settingsService.getUseExternalPlayer(); _useExternalPlayer = _settingsService.read(settings.SettingsService.useExternalPlayer);
_selectedExternalPlayerName = _settingsService.getSelectedExternalPlayer().name; _selectedExternalPlayerName = _settingsService.read(settings.SettingsService.selectedExternalPlayer).name;
_defaultQualityPreset = TranscodeQualityPreset.fromStorage(_settingsService.getDefaultQualityPreset()); _defaultQualityPreset = TranscodeQualityPreset.fromStorage(
_settingsService.read(settings.SettingsService.defaultQualityPreset),
);
_isLoading = false; _isLoading = false;
}); });
} }
@@ -182,7 +184,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
selected: _useExoPlayer, selected: _useExoPlayer,
onChanged: (value) async { onChanged: (value) async {
setState(() => _useExoPlayer = value); setState(() => _useExoPlayer = value);
await _settingsService.setUseExoPlayer(value); await _settingsService.write(settings.SettingsService.useExoPlayer, value);
}, },
); );
} }
@@ -198,8 +200,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
final s = await settings.SettingsService.getInstance(); final s = await settings.SettingsService.getInstance();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_useExternalPlayer = s.getUseExternalPlayer(); _useExternalPlayer = s.read(settings.SettingsService.useExternalPlayer);
_selectedExternalPlayerName = s.getSelectedExternalPlayer().name; _selectedExternalPlayerName = s.read(settings.SettingsService.selectedExternalPlayer).name;
}); });
}, },
); );
@@ -213,7 +215,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _enableHardwareDecoding, value: _enableHardwareDecoding,
onChanged: (value) async { onChanged: (value) async {
setState(() => _enableHardwareDecoding = value); setState(() => _enableHardwareDecoding = value);
await _settingsService.setEnableHardwareDecoding(value); await _settingsService.write(settings.SettingsService.enableHardwareDecoding, value);
}, },
); );
} }
@@ -226,7 +228,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _autoPip, value: _autoPip,
onChanged: (value) async { onChanged: (value) async {
setState(() => _autoPip = value); setState(() => _autoPip = value);
await _settingsService.setAutoPip(value); await _settingsService.write(settings.SettingsService.autoPip, value);
}, },
); );
} }
@@ -239,7 +241,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _matchContentFrameRate, value: _matchContentFrameRate,
onChanged: (value) async { onChanged: (value) async {
setState(() => _matchContentFrameRate = value); setState(() => _matchContentFrameRate = value);
await _settingsService.setMatchContentFrameRate(value); await _settingsService.write(settings.SettingsService.matchContentFrameRate, value);
}, },
); );
} }
@@ -252,7 +254,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _matchRefreshRate, value: _matchRefreshRate,
onChanged: (value) async { onChanged: (value) async {
setState(() => _matchRefreshRate = value); setState(() => _matchRefreshRate = value);
await _settingsService.setMatchRefreshRate(value); await _settingsService.write(settings.SettingsService.matchRefreshRate, value);
}, },
); );
} }
@@ -265,7 +267,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _matchDynamicRange, value: _matchDynamicRange,
onChanged: (value) async { onChanged: (value) async {
setState(() => _matchDynamicRange = value); setState(() => _matchDynamicRange = value);
await _settingsService.setMatchDynamicRange(value); await _settingsService.write(settings.SettingsService.matchDynamicRange, value);
}, },
); );
} }
@@ -286,7 +288,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
currentValue: _displaySwitchDelay, currentValue: _displaySwitchDelay,
onSave: (value) async { onSave: (value) async {
setState(() => _displaySwitchDelay = value); setState(() => _displaySwitchDelay = value);
await _settingsService.setDisplaySwitchDelay(value); await _settingsService.write(settings.SettingsService.displaySwitchDelay, value);
}, },
), ),
); );
@@ -300,7 +302,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _tunneledPlayback, value: _tunneledPlayback,
onChanged: (value) async { onChanged: (value) async {
setState(() => _tunneledPlayback = value); setState(() => _tunneledPlayback = value);
await _settingsService.setTunneledPlayback(value); await _settingsService.write(settings.SettingsService.tunneledPlayback, value);
}, },
); );
} }
@@ -323,7 +325,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
if (value != null) { if (value != null) {
setState(() { setState(() {
_defaultQualityPreset = value; _defaultQualityPreset = value;
_settingsService.setDefaultQualityPreset(value.storageKey); _settingsService.write(settings.SettingsService.defaultQualityPreset, value.storageKey);
}); });
} }
}, },
@@ -351,7 +353,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
if (value != null) { if (value != null) {
setState(() { setState(() {
_bufferSize = value; _bufferSize = value;
_settingsService.setBufferSize(value); _settingsService.write(settings.SettingsService.bufferSize, value);
}); });
if (Platform.isAndroid && value > 0) { if (Platform.isAndroid && value > 0) {
final heapMB = await PlayerAndroid.getHeapSize(); final heapMB = await PlayerAndroid.getHeapSize();
@@ -409,7 +411,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
onSave: (value) async { onSave: (value) async {
setState(() { setState(() {
_seekTimeSmall = value; _seekTimeSmall = value;
_settingsService.setSeekTimeSmall(value); _settingsService.write(settings.SettingsService.seekTimeSmall, value);
}); });
await _keyboardService?.refreshFromStorage(); await _keyboardService?.refreshFromStorage();
}, },
@@ -434,7 +436,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
onSave: (value) async { onSave: (value) async {
setState(() { setState(() {
_seekTimeLarge = value; _seekTimeLarge = value;
_settingsService.setSeekTimeLarge(value); _settingsService.write(settings.SettingsService.seekTimeLarge, value);
}); });
await _keyboardService?.refreshFromStorage(); await _keyboardService?.refreshFromStorage();
}, },
@@ -459,7 +461,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
onSave: (value) async { onSave: (value) async {
setState(() { setState(() {
_rewindOnResume = value; _rewindOnResume = value;
_settingsService.setRewindOnResume(value); _settingsService.write(settings.SettingsService.rewindOnResume, value);
}); });
}, },
), ),
@@ -482,7 +484,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
currentValue: _sleepTimerDuration, currentValue: _sleepTimerDuration,
onSave: (value) async { onSave: (value) async {
setState(() => _sleepTimerDuration = value); setState(() => _sleepTimerDuration = value);
await _settingsService.setSleepTimerDuration(value); await _settingsService.write(settings.SettingsService.sleepTimerDuration, value);
}, },
), ),
); );
@@ -504,7 +506,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
currentValue: _maxVolume, currentValue: _maxVolume,
onSave: (value) async { onSave: (value) async {
setState(() => _maxVolume = value); setState(() => _maxVolume = value);
await _settingsService.setMaxVolume(value); await _settingsService.write(settings.SettingsService.maxVolume, value);
}, },
), ),
); );
@@ -520,7 +522,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _enableDiscordRPC, value: _enableDiscordRPC,
onChanged: (value) async { onChanged: (value) async {
setState(() => _enableDiscordRPC = value); setState(() => _enableDiscordRPC = value);
await _settingsService.setEnableDiscordRPC(value); await _settingsService.write(settings.SettingsService.enableDiscordRPC, value);
await DiscordRPCService.instance.setEnabled(value); await DiscordRPCService.instance.setEnabled(value);
}, },
); );
@@ -534,7 +536,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _enableCompanionRemoteServer, value: _enableCompanionRemoteServer,
onChanged: (value) async { onChanged: (value) async {
setState(() => _enableCompanionRemoteServer = value); setState(() => _enableCompanionRemoteServer = value);
await _settingsService.setEnableCompanionRemoteServer(value); await _settingsService.write(settings.SettingsService.enableCompanionRemoteServer, value);
}, },
); );
} }
@@ -547,7 +549,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _rememberTrackSelections, value: _rememberTrackSelections,
onChanged: (value) async { onChanged: (value) async {
setState(() => _rememberTrackSelections = value); setState(() => _rememberTrackSelections = value);
await _settingsService.setRememberTrackSelections(value); await _settingsService.write(settings.SettingsService.rememberTrackSelections, value);
}, },
); );
} }
@@ -560,7 +562,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _clickVideoTogglesPlayback, value: _clickVideoTogglesPlayback,
onChanged: (value) async { onChanged: (value) async {
setState(() => _clickVideoTogglesPlayback = value); setState(() => _clickVideoTogglesPlayback = value);
await _settingsService.setClickVideoTogglesPlayback(value); await _settingsService.write(settings.SettingsService.clickVideoTogglesPlayback, value);
}, },
); );
} }
@@ -575,7 +577,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _autoSkipIntro, value: _autoSkipIntro,
onChanged: (value) async { onChanged: (value) async {
setState(() => _autoSkipIntro = value); setState(() => _autoSkipIntro = value);
await _settingsService.setAutoSkipIntro(value); await _settingsService.write(settings.SettingsService.autoSkipIntro, value);
}, },
); );
} }
@@ -588,7 +590,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
value: _autoSkipCredits, value: _autoSkipCredits,
onChanged: (value) async { onChanged: (value) async {
setState(() => _autoSkipCredits = value); setState(() => _autoSkipCredits = value);
await _settingsService.setAutoSkipCredits(value); await _settingsService.write(settings.SettingsService.autoSkipCredits, value);
}, },
); );
} }
@@ -609,7 +611,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
currentValue: _autoSkipDelay, currentValue: _autoSkipDelay,
onSave: (value) async { onSave: (value) async {
setState(() => _autoSkipDelay = value); setState(() => _autoSkipDelay = value);
await _settingsService.setAutoSkipDelay(value); await _settingsService.write(settings.SettingsService.autoSkipDelay, value);
}, },
), ),
); );
@@ -628,7 +630,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
defaultValue: settings.SettingsService.defaultIntroPattern, defaultValue: settings.SettingsService.defaultIntroPattern,
onSave: (value) async { onSave: (value) async {
setState(() => _introPattern = value); setState(() => _introPattern = value);
await _settingsService.setIntroPattern(value); await _settingsService.write(settings.SettingsService.introPattern, value);
}, },
), ),
); );
@@ -647,7 +649,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
defaultValue: settings.SettingsService.defaultCreditsPattern, defaultValue: settings.SettingsService.defaultCreditsPattern,
onSave: (value) async { onSave: (value) async {
setState(() => _creditsPattern = value); setState(() => _creditsPattern = value);
await _settingsService.setCreditsPattern(value); await _settingsService.write(settings.SettingsService.creditsPattern, value);
}, },
), ),
); );
+20 -18
View File
@@ -136,13 +136,13 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_crashReporting = _settingsService.getCrashReporting(); _crashReporting = _settingsService.read(settings.SettingsService.crashReporting);
_enableDebugLogging = _settingsService.getEnableDebugLogging(); _enableDebugLogging = _settingsService.read(settings.SettingsService.enableDebugLogging);
_downloadOnWifiOnly = _settingsService.getDownloadOnWifiOnly(); _downloadOnWifiOnly = _settingsService.read(settings.SettingsService.downloadOnWifiOnly);
_autoRemoveWatchedDownloads = _settingsService.getAutoRemoveWatchedDownloads(); _autoRemoveWatchedDownloads = _settingsService.read(settings.SettingsService.autoRemoveWatchedDownloads);
_autoCheckUpdatesOnStartup = _settingsService.getAutoCheckUpdatesOnStartup(); _autoCheckUpdatesOnStartup = _settingsService.read(settings.SettingsService.autoCheckUpdatesOnStartup);
_videoPlayerNavigationEnabled = _settingsService.getVideoPlayerNavigationEnabled(); _videoPlayerNavigationEnabled = _settingsService.read(settings.SettingsService.videoPlayerNavigationEnabled);
_customRelayUrl = _settingsService.getCustomRelayUrl(); _customRelayUrl = _settingsService.read(settings.SettingsService.customRelayUrl);
_isLoading = false; _isLoading = false;
}); });
} }
@@ -312,7 +312,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
value: _downloadOnWifiOnly, value: _downloadOnWifiOnly,
onChanged: (value) async { onChanged: (value) async {
setState(() => _downloadOnWifiOnly = value); setState(() => _downloadOnWifiOnly = value);
await _settingsService.setDownloadOnWifiOnly(value); await _settingsService.write(settings.SettingsService.downloadOnWifiOnly, value);
}, },
), ),
SwitchListTile( SwitchListTile(
@@ -323,7 +323,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
value: _autoRemoveWatchedDownloads, value: _autoRemoveWatchedDownloads,
onChanged: (value) async { onChanged: (value) async {
setState(() => _autoRemoveWatchedDownloads = value); setState(() => _autoRemoveWatchedDownloads = value);
await _settingsService.setAutoRemoveWatchedDownloads(value); await _settingsService.write(settings.SettingsService.autoRemoveWatchedDownloads, value);
}, },
), ),
], ],
@@ -358,7 +358,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
value: _videoPlayerNavigationEnabled, value: _videoPlayerNavigationEnabled,
onChanged: (value) async { onChanged: (value) async {
setState(() => _videoPlayerNavigationEnabled = value); setState(() => _videoPlayerNavigationEnabled = value);
await _settingsService.setVideoPlayerNavigationEnabled(value); await _settingsService.write(settings.SettingsService.videoPlayerNavigationEnabled, value);
}, },
), ),
], ],
@@ -386,7 +386,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
value: _crashReporting, value: _crashReporting,
onChanged: (value) async { onChanged: (value) async {
setState(() => _crashReporting = value); setState(() => _crashReporting = value);
await _settingsService.setCrashReporting(value); await _settingsService.write(settings.SettingsService.crashReporting, value);
}, },
), ),
SwitchListTile( SwitchListTile(
@@ -397,7 +397,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
value: _enableDebugLogging, value: _enableDebugLogging,
onChanged: (value) async { onChanged: (value) async {
setState(() => _enableDebugLogging = value); setState(() => _enableDebugLogging = value);
await _settingsService.setEnableDebugLogging(value); await _settingsService.write(settings.SettingsService.enableDebugLogging, value);
}, },
), ),
ListTile( ListTile(
@@ -486,7 +486,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
value: _autoCheckUpdatesOnStartup, value: _autoCheckUpdatesOnStartup,
onChanged: (value) async { onChanged: (value) async {
setState(() => _autoCheckUpdatesOnStartup = value); setState(() => _autoCheckUpdatesOnStartup = value);
await _settingsService.setAutoCheckUpdatesOnStartup(value); await _settingsService.write(settings.SettingsService.autoCheckUpdatesOnStartup, value);
}, },
); );
} }
@@ -625,7 +625,8 @@ class _SettingsScreenState extends State<SettingsScreen> 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(); await DownloadStorageService.instance.refreshCustomPath();
if (mounted) { if (mounted) {
@@ -642,7 +643,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
} }
Future<void> _resetDownloadLocation() async { Future<void> _resetDownloadLocation() async {
await _settingsService.setCustomDownloadPath(null); await _settingsService.write(settings.SettingsService.customDownloadPath, null);
await _settingsService.write(settings.SettingsService.customDownloadPathType, null);
await DownloadStorageService.instance.refreshCustomPath(); await DownloadStorageService.instance.refreshCustomPath();
if (mounted) { if (mounted) {
@@ -672,7 +674,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
DialogActionButton( DialogActionButton(
onPressed: () async { onPressed: () async {
controller.clear(); controller.clear();
await _settingsService.setCustomRelayUrl(null); await _settingsService.write(settings.SettingsService.customRelayUrl, null);
if (mounted) setState(() => _customRelayUrl = null); if (mounted) setState(() => _customRelayUrl = null);
if (dialogContext.mounted) Navigator.pop(dialogContext); if (dialogContext.mounted) Navigator.pop(dialogContext);
}, },
@@ -683,7 +685,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
focusNode: saveFocusNode, focusNode: saveFocusNode,
onPressed: () async { onPressed: () async {
final url = controller.text.trim().isEmpty ? null : controller.text.trim(); 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 (mounted) setState(() => _customRelayUrl = url);
if (dialogContext.mounted) Navigator.pop(dialogContext); if (dialogContext.mounted) Navigator.pop(dialogContext);
}, },
@@ -764,7 +766,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
if (!mounted) return; if (!mounted) return;
if (result == null) return; // user cancelled file picker if (result == null) return; // user cancelled file picker
LocaleSettings.setLocale(_settingsService.getAppLocale()); LocaleSettings.setLocale(_settingsService.read(settings.SettingsService.appLocale));
await Future.wait([ await Future.wait([
themeProvider.reload(), themeProvider.reload(),
settingsProvider.reload(), settingsProvider.reload(),
@@ -44,16 +44,16 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_fontSize = _settingsService.getSubtitleFontSize(); _fontSize = _settingsService.read(SettingsService.subtitleFontSize);
_textColor = _settingsService.getSubtitleTextColor(); _textColor = _settingsService.read(SettingsService.subtitleTextColor);
_borderSize = _settingsService.getSubtitleBorderSize(); _borderSize = _settingsService.read(SettingsService.subtitleBorderSize);
_borderColor = _settingsService.getSubtitleBorderColor(); _borderColor = _settingsService.read(SettingsService.subtitleBorderColor);
_backgroundColor = _settingsService.getSubtitleBackgroundColor(); _backgroundColor = _settingsService.read(SettingsService.subtitleBackgroundColor);
_backgroundOpacity = _settingsService.getSubtitleBackgroundOpacity(); _backgroundOpacity = _settingsService.read(SettingsService.subtitleBackgroundOpacity);
_subtitlePosition = _settingsService.getSubtitlePosition(); _subtitlePosition = _settingsService.read(SettingsService.subtitlePosition);
_assOverride = _settingsService.getSubAssOverride(); _assOverride = _settingsService.read(SettingsService.subAssOverride);
_bold = _settingsService.getSubtitleBold(); _bold = _settingsService.read(SettingsService.subtitleBold);
_italic = _settingsService.getSubtitleItalic(); _italic = _settingsService.read(SettingsService.subtitleItalic);
_isLoading = false; _isLoading = false;
}); });
} }
@@ -200,7 +200,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
); );
if (value != null) { if (value != null) {
setState(() => _assOverride = value); setState(() => _assOverride = value);
await _settingsService.setSubAssOverride(value); await _settingsService.write(SettingsService.subAssOverride, value);
} }
}, },
), ),
@@ -219,7 +219,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
currentValue: _fontSize, currentValue: _fontSize,
onSave: (value) async { onSave: (value) async {
setState(() => _fontSize = value); setState(() => _fontSize = value);
await _settingsService.setSubtitleFontSize(value); await _settingsService.write(SettingsService.subtitleFontSize, value);
}, },
), ),
), ),
@@ -238,7 +238,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _openColorPicker(t.subtitlingStyling.textColor, _textColor, (color) { onTap: () => _openColorPicker(t.subtitlingStyling.textColor, _textColor, (color) {
setState(() => _textColor = color); setState(() => _textColor = color);
_settingsService.setSubtitleTextColor(color); _settingsService.write(SettingsService.subtitleTextColor, color);
}), }),
), ),
ListTile( ListTile(
@@ -256,7 +256,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
currentValue: _subtitlePosition, currentValue: _subtitlePosition,
onSave: (value) async { onSave: (value) async {
setState(() => _subtitlePosition = value); setState(() => _subtitlePosition = value);
await _settingsService.setSubtitlePosition(value); await _settingsService.write(SettingsService.subtitlePosition, value);
}, },
), ),
), ),
@@ -266,7 +266,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
value: _bold, value: _bold,
onChanged: (value) async { onChanged: (value) async {
setState(() => _bold = value); setState(() => _bold = value);
await _settingsService.setSubtitleBold(value); await _settingsService.write(SettingsService.subtitleBold, value);
}, },
), ),
SwitchListTile( SwitchListTile(
@@ -275,7 +275,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
value: _italic, value: _italic,
onChanged: (value) async { onChanged: (value) async {
setState(() => _italic = value); setState(() => _italic = value);
await _settingsService.setSubtitleItalic(value); await _settingsService.write(SettingsService.subtitleItalic, value);
}, },
), ),
@@ -296,7 +296,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
currentValue: _borderSize, currentValue: _borderSize,
onSave: (value) async { onSave: (value) async {
setState(() => _borderSize = value); setState(() => _borderSize = value);
await _settingsService.setSubtitleBorderSize(value); await _settingsService.write(SettingsService.subtitleBorderSize, value);
}, },
), ),
), ),
@@ -315,7 +315,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _openColorPicker(t.subtitlingStyling.borderColor, _borderColor, (color) { onTap: () => _openColorPicker(t.subtitlingStyling.borderColor, _borderColor, (color) {
setState(() => _borderColor = color); setState(() => _borderColor = color);
_settingsService.setSubtitleBorderColor(color); _settingsService.write(SettingsService.subtitleBorderColor, color);
}), }),
), ),
@@ -336,7 +336,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
currentValue: _backgroundOpacity, currentValue: _backgroundOpacity,
onSave: (value) async { onSave: (value) async {
setState(() => _backgroundOpacity = value); setState(() => _backgroundOpacity = value);
await _settingsService.setSubtitleBackgroundOpacity(value); await _settingsService.write(SettingsService.subtitleBackgroundOpacity, value);
}, },
), ),
), ),
@@ -355,7 +355,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _openColorPicker(t.subtitlingStyling.backgroundColor, _backgroundColor, (color) { onTap: () => _openColorPicker(t.subtitlingStyling.backgroundColor, _backgroundColor, (color) {
setState(() => _backgroundColor = color); setState(() => _backgroundColor = color);
_settingsService.setSubtitleBackgroundColor(color); _settingsService.write(SettingsService.subtitleBackgroundColor, color);
}), }),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -23,8 +23,8 @@ class TrackerLibraryFilterScreen extends StatefulWidget {
/// Human-readable summary of the current filter state for [service], used as /// Human-readable summary of the current filter state for [service], used as
/// the subtitle on the parent settings screen's "Library filter" tile. /// the subtitle on the parent settings screen's "Library filter" tile.
static String subtitleFor(SettingsService settings, TrackerService service) { static String subtitleFor(SettingsService settings, TrackerService service) {
final mode = settings.getTrackerLibraryFilterMode(service); final mode = settings.read(SettingsService.trackerFilterModePref(service));
final ids = settings.getTrackerLibraryFilterIds(service); final ids = settings.read(SettingsService.trackerFilterIdsPref(service)).toSet();
if (ids.isEmpty) { if (ids.isEmpty) {
return mode == TrackerLibraryFilterMode.blacklist return mode == TrackerLibraryFilterMode.blacklist
? t.trackers.libraryFilter.subtitleAllSyncing ? t.trackers.libraryFilter.subtitleAllSyncing
@@ -57,10 +57,10 @@ class _TrackerLibraryFilterScreenState extends State<TrackerLibraryFilterScreen>
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_settings = s; _settings = s;
_mode = s.getTrackerLibraryFilterMode(widget.service); _mode = s.read(SettingsService.trackerFilterModePref(widget.service));
_selectedIds _selectedIds
..clear() ..clear()
..addAll(s.getTrackerLibraryFilterIds(widget.service)); ..addAll(s.read(SettingsService.trackerFilterIdsPref(widget.service)).toSet());
_loaded = true; _loaded = true;
}); });
} }
@@ -68,7 +68,7 @@ class _TrackerLibraryFilterScreenState extends State<TrackerLibraryFilterScreen>
Future<void> _setMode(TrackerLibraryFilterMode mode) async { Future<void> _setMode(TrackerLibraryFilterMode mode) async {
if (mode == _mode) return; if (mode == _mode) return;
setState(() => _mode = mode); setState(() => _mode = mode);
await _settings.setTrackerLibraryFilterMode(widget.service, mode); await _settings.write(SettingsService.trackerFilterModePref(widget.service), mode);
} }
Future<void> _toggleLibrary(String globalKey, bool value) async { Future<void> _toggleLibrary(String globalKey, bool value) async {
@@ -79,7 +79,7 @@ class _TrackerLibraryFilterScreenState extends State<TrackerLibraryFilterScreen>
_selectedIds.remove(globalKey); _selectedIds.remove(globalKey);
} }
}); });
await _settings.setTrackerLibraryFilterIds(widget.service, _selectedIds); await _settings.write(SettingsService.trackerFilterIdsPref(widget.service), _selectedIds.toList());
} }
@override @override
@@ -88,9 +88,9 @@ class TrackerConfig {
displayName: t.trackers.services.mal, displayName: t.trackers.services.mal,
isConnected: (a) => a.isMalConnected, isConnected: (a) => a.isMalConnected,
username: (a) => a.malUsername, username: (a) => a.malUsername,
readScrobbleEnabled: (s) => s.getEnableMalScrobble(), readScrobbleEnabled: (s) => s.read(SettingsService.enableMalScrobble),
setScrobbleEnabled: (s, v) async { setScrobbleEnabled: (s, v) async {
await s.setEnableMalScrobble(v); await s.write(SettingsService.enableMalScrobble, v);
await MalTracker.instance.setEnabled(v); await MalTracker.instance.setEnabled(v);
}, },
disconnect: (a) => a.disconnectMal(), disconnect: (a) => a.disconnectMal(),
@@ -101,9 +101,9 @@ class TrackerConfig {
displayName: t.trackers.services.anilist, displayName: t.trackers.services.anilist,
isConnected: (a) => a.isAnilistConnected, isConnected: (a) => a.isAnilistConnected,
username: (a) => a.anilistUsername, username: (a) => a.anilistUsername,
readScrobbleEnabled: (s) => s.getEnableAnilistScrobble(), readScrobbleEnabled: (s) => s.read(SettingsService.enableAnilistScrobble),
setScrobbleEnabled: (s, v) async { setScrobbleEnabled: (s, v) async {
await s.setEnableAnilistScrobble(v); await s.write(SettingsService.enableAnilistScrobble, v);
await AnilistTracker.instance.setEnabled(v); await AnilistTracker.instance.setEnabled(v);
}, },
disconnect: (a) => a.disconnectAnilist(), disconnect: (a) => a.disconnectAnilist(),
@@ -114,9 +114,9 @@ class TrackerConfig {
displayName: t.trackers.services.simkl, displayName: t.trackers.services.simkl,
isConnected: (a) => a.isSimklConnected, isConnected: (a) => a.isSimklConnected,
username: (a) => a.simklUsername, username: (a) => a.simklUsername,
readScrobbleEnabled: (s) => s.getEnableSimklScrobble(), readScrobbleEnabled: (s) => s.read(SettingsService.enableSimklScrobble),
setScrobbleEnabled: (s, v) async { setScrobbleEnabled: (s, v) async {
await s.setEnableSimklScrobble(v); await s.write(SettingsService.enableSimklScrobble, v);
await SimklTracker.instance.setEnabled(v); await SimklTracker.instance.setEnabled(v);
}, },
disconnect: (a) => a.disconnectSimkl(), disconnect: (a) => a.disconnectSimkl(),
@@ -55,8 +55,8 @@ class _TraktSettingsScreenState extends State<TraktSettingsScreen> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_settings = s; _settings = s;
_scrobbleEnabled = s.getEnableTraktScrobble(); _scrobbleEnabled = s.read(SettingsService.enableTraktScrobble);
_watchedSyncEnabled = s.getEnableTraktWatchedSync(); _watchedSyncEnabled = s.read(SettingsService.enableTraktWatchedSync);
_loaded = true; _loaded = true;
}); });
} }
@@ -118,7 +118,7 @@ class _TraktSettingsScreenState extends State<TraktSettingsScreen> {
value: _scrobbleEnabled, value: _scrobbleEnabled,
onChanged: (value) async { onChanged: (value) async {
setState(() => _scrobbleEnabled = value); setState(() => _scrobbleEnabled = value);
await _settings!.setEnableTraktScrobble(value); await _settings!.write(SettingsService.enableTraktScrobble, value);
await TraktScrobbleService.instance.setEnabled(value); await TraktScrobbleService.instance.setEnabled(value);
}, },
), ),
@@ -129,7 +129,7 @@ class _TraktSettingsScreenState extends State<TraktSettingsScreen> {
value: _watchedSyncEnabled, value: _watchedSyncEnabled,
onChanged: (value) async { onChanged: (value) async {
setState(() => _watchedSyncEnabled = value); setState(() => _watchedSyncEnabled = value);
await _settings!.setEnableTraktWatchedSync(value); await _settings!.write(SettingsService.enableTraktWatchedSync, value);
await TraktSyncService.instance.setEnabled(value); await TraktSyncService.instance.setEnabled(value);
}, },
), ),
+58 -55
View File
@@ -602,13 +602,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
try { try {
// Load buffer size from settings // Load buffer size from settings
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
_videoPlayerNavigationEnabled = settingsService.getVideoPlayerNavigationEnabled(); _videoPlayerNavigationEnabled = settingsService.read(SettingsService.videoPlayerNavigationEnabled);
_autoPipEnabled = settingsService.getAutoPip(); _autoPipEnabled = settingsService.read(SettingsService.autoPip);
_rewindOnResume = settingsService.getRewindOnResume(); _rewindOnResume = settingsService.read(SettingsService.rewindOnResume);
final bufferSizeMB = settingsService.getBufferSize(); final bufferSizeMB = settingsService.read(SettingsService.bufferSize);
final enableHardwareDecoding = settingsService.getEnableHardwareDecoding(); final enableHardwareDecoding = settingsService.read(SettingsService.enableHardwareDecoding);
final debugLoggingEnabled = settingsService.getEnableDebugLogging(); final debugLoggingEnabled = settingsService.read(SettingsService.enableDebugLogging);
final useExoPlayer = settingsService.getUseExoPlayer(); final useExoPlayer = settingsService.read(SettingsService.useExoPlayer);
// Initialize Windows display mode service. // Initialize Windows display mode service.
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -667,7 +667,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await player!.configureSubtitleFonts(); await player!.configureSubtitleFonts();
await player!.setProperty('sub-ass', 'yes'); // Enable libass await player!.setProperty('sub-ass', 'yes'); // Enable libass
if (Platform.isAndroid && useExoPlayer) { if (Platform.isAndroid && useExoPlayer) {
final tunneledPlayback = settingsService.getTunneledPlayback(); final tunneledPlayback = settingsService.read(SettingsService.tunneledPlayback);
await player!.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no'); await player!.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no');
} }
if (bufferSizeMB > 0) { if (bufferSizeMB > 0) {
@@ -716,24 +716,24 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding));
// Subtitle styling // Subtitle styling
await player!.setProperty('sub-font-size', settingsService.getSubtitleFontSize().toString()); await player!.setProperty('sub-font-size', settingsService.read(SettingsService.subtitleFontSize).toString());
await player!.setProperty('sub-color', settingsService.getSubtitleTextColor()); await player!.setProperty('sub-color', settingsService.read(SettingsService.subtitleTextColor));
await player!.setProperty('sub-border-size', settingsService.getSubtitleBorderSize().toString()); await player!.setProperty('sub-border-size', settingsService.read(SettingsService.subtitleBorderSize).toString());
await player!.setProperty('sub-border-color', settingsService.getSubtitleBorderColor()); await player!.setProperty('sub-border-color', settingsService.read(SettingsService.subtitleBorderColor));
await player!.setProperty('sub-bold', settingsService.getSubtitleBold() ? 'yes' : 'no'); await player!.setProperty('sub-bold', settingsService.read(SettingsService.subtitleBold) ? 'yes' : 'no');
await player!.setProperty('sub-italic', settingsService.getSubtitleItalic() ? 'yes' : 'no'); await player!.setProperty('sub-italic', settingsService.read(SettingsService.subtitleItalic) ? 'yes' : 'no');
final bgOpacity = (settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt(); final bgOpacity = (settingsService.read(SettingsService.subtitleBackgroundOpacity) * 255 / 100).toInt();
final bgColor = settingsService.getSubtitleBackgroundColor().replaceFirst('#', ''); final bgColor = settingsService.read(SettingsService.subtitleBackgroundColor).replaceFirst('#', '');
await player!.setProperty( await player!.setProperty(
'sub-back-color', 'sub-back-color',
'#${bgOpacity.toRadixString(16).padLeft(2, '0').toUpperCase()}$bgColor', '#${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-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-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 // Platform-specific settings
if (Platform.isIOS) { if (Platform.isIOS) {
@@ -742,38 +742,38 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Audio passthrough (desktop only - sends bitstream to receiver) // Audio passthrough (desktop only - sends bitstream to receiver)
if (PlatformDetector.isDesktopOS()) { if (PlatformDetector.isDesktopOS()) {
if (settingsService.getAudioPassthrough()) { if (settingsService.read(SettingsService.audioPassthrough)) {
await player!.setAudioPassthrough(true); await player!.setAudioPassthrough(true);
} }
} }
// HDR is controlled via custom hdr-enabled property on iOS/macOS/Windows // HDR is controlled via custom hdr-enabled property on iOS/macOS/Windows
if (Platform.isIOS || Platform.isMacOS || Platform.isWindows) { 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'); await player!.setProperty('hdr-enabled', enableHDR ? 'yes' : 'no');
} }
// Apply audio sync offset // Apply audio sync offset
final audioSyncOffset = settingsService.getAudioSyncOffset(); final audioSyncOffset = settingsService.read(SettingsService.audioSyncOffset);
if (audioSyncOffset != 0) { if (audioSyncOffset != 0) {
final offsetSeconds = audioSyncOffset / 1000.0; final offsetSeconds = audioSyncOffset / 1000.0;
await player!.setProperty('audio-delay', offsetSeconds.toString()); await player!.setProperty('audio-delay', offsetSeconds.toString());
} }
// Apply subtitle sync offset // Apply subtitle sync offset
final subtitleSyncOffset = settingsService.getSubtitleSyncOffset(); final subtitleSyncOffset = settingsService.read(SettingsService.subtitleSyncOffset);
if (subtitleSyncOffset != 0) { if (subtitleSyncOffset != 0) {
final offsetSeconds = subtitleSyncOffset / 1000.0; final offsetSeconds = subtitleSyncOffset / 1000.0;
await player!.setProperty('sub-delay', offsetSeconds.toString()); await player!.setProperty('sub-delay', offsetSeconds.toString());
} }
// Apply audio normalization (loudnorm filter) // Apply audio normalization (loudnorm filter)
if (settingsService.getAudioNormalization()) { if (settingsService.read(SettingsService.audioNormalization)) {
await player!.setProperty('af', 'loudnorm=I=-14:TP=-3:LRA=4'); await player!.setProperty('af', 'loudnorm=I=-14:TP=-3:LRA=4');
} }
// Apply custom MPV config entries // Apply custom MPV config entries
final customMpvConfig = settingsService.getEnabledMpvConfigEntries(); final customMpvConfig = SettingsService.parseMpvConfigText(settingsService.read(SettingsService.mpvConfigText));
for (final entry in customMpvConfig.entries) { for (final entry in customMpvConfig.entries) {
try { try {
await player!.setProperty(entry.key, entry.value); await player!.setProperty(entry.key, entry.value);
@@ -784,11 +784,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
// Set max volume limit for volume boost // Set max volume limit for volume boost
final maxVolume = settingsService.getMaxVolume(); final maxVolume = settingsService.read(SettingsService.maxVolume);
await player!.setProperty('volume-max', maxVolume.toString()); await player!.setProperty('volume-max', maxVolume.toString());
// Apply saved volume (clamped to max volume) // 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); player!.setVolume(savedVolume);
// Notify that player is ready // Notify that player is ready
@@ -815,7 +815,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
if (mounted) { if (mounted) {
try { try {
// Check rotation lock setting before applying orientation // Check rotation lock setting before applying orientation
final isRotationLocked = settingsService.getRotationLocked(); final isRotationLocked = settingsService.read(SettingsService.rotationLocked);
if (isRotationLocked) { if (isRotationLocked) {
// Locked: Apply landscape orientation only // Locked: Apply landscape orientation only
@@ -906,7 +906,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')); Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player'));
// Apply frame rate matching on Android if enabled // Apply frame rate matching on Android if enabled
if (Platform.isAndroid && settingsService.getMatchContentFrameRate()) { if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) {
await _applyFrameRateMatching(); await _applyFrameRateMatching();
} }
@@ -933,7 +933,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Apply frame rate matching here too, since this fallback may fire // Apply frame rate matching here too, since this fallback may fire
// before playbackRestart (race condition with resume positions > 0) // before playbackRestart (race condition with resume positions > 0)
if (Platform.isAndroid && settingsService.getMatchContentFrameRate()) { if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) {
_applyFrameRateMatching(); _applyFrameRateMatching();
} }
} }
@@ -1005,7 +1005,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_frameRateMatchingApplied = true; _frameRateMatchingApplied = true;
final durationMs = player!.state.duration.inMilliseconds; final durationMs = player!.state.duration.inMilliseconds;
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
final delaySec = settingsService.getDisplaySwitchDelay(); final delaySec = settingsService.read(SettingsService.displaySwitchDelay);
// Suppress spurious PauseEvent from MediaSession during HDMI renegotiation. // Suppress spurious PauseEvent from MediaSession during HDMI renegotiation.
// Fire Stick (and similar Android TV devices) send onPause() through the // Fire Stick (and similar Android TV devices) send onPause() through the
@@ -1525,7 +1525,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
final preKnownFps = result.mediaInfo?.frameRate; final preKnownFps = result.mediaInfo?.frameRate;
final willAutoSwitch = final willAutoSwitch =
Platform.isAndroid && settingsService.getMatchContentFrameRate() && preKnownFps != null && preKnownFps > 0; Platform.isAndroid &&
settingsService.read(SettingsService.matchContentFrameRate) &&
preKnownFps != null &&
preKnownFps > 0;
// Open video through Player // Open video through Player
if (result.videoUrl != null) { if (result.videoUrl != null) {
@@ -1588,15 +1591,15 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Must be called after open() since that's when ExoPlayer initializes // Must be called after open() since that's when ExoPlayer initializes
if (player is PlayerAndroid) { if (player is PlayerAndroid) {
await (player as PlayerAndroid).setSubtitleStyle( await (player as PlayerAndroid).setSubtitleStyle(
fontSize: settingsService.getSubtitleFontSize().toDouble(), fontSize: settingsService.read(SettingsService.subtitleFontSize).toDouble(),
textColor: settingsService.getSubtitleTextColor(), textColor: settingsService.read(SettingsService.subtitleTextColor),
borderSize: settingsService.getSubtitleBorderSize().toDouble(), borderSize: settingsService.read(SettingsService.subtitleBorderSize).toDouble(),
borderColor: settingsService.getSubtitleBorderColor(), borderColor: settingsService.read(SettingsService.subtitleBorderColor),
bgColor: settingsService.getSubtitleBackgroundColor(), bgColor: settingsService.read(SettingsService.subtitleBackgroundColor),
bgOpacity: settingsService.getSubtitleBackgroundOpacity(), bgOpacity: settingsService.read(SettingsService.subtitleBackgroundOpacity),
subtitlePosition: settingsService.getSubtitlePosition(), subtitlePosition: settingsService.read(SettingsService.subtitlePosition),
bold: settingsService.getSubtitleBold(), bold: settingsService.read(SettingsService.subtitleBold),
italic: settingsService.getSubtitleItalic(), italic: settingsService.read(SettingsService.subtitleItalic),
); );
} }
@@ -1714,7 +1717,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// the switch has settled. // the switch has settled.
if (willAutoSwitch && mounted && player != null) { if (willAutoSwitch && mounted && player != null) {
_frameRateMatchingApplied = true; _frameRateMatchingApplied = true;
final delaySec = settingsService.getDisplaySwitchDelay(); final delaySec = settingsService.read(SettingsService.displaySwitchDelay);
final durationMs = _currentMetadata.duration ?? player!.state.duration.inMilliseconds; final durationMs = _currentMetadata.duration ?? player!.state.duration.inMilliseconds;
_suppressMediaPauseDuringFrameRateSwitch = true; _suppressMediaPauseDuringFrameRateSwitch = true;
Future.delayed(Duration(seconds: 2 + delaySec + 1), () { Future.delayed(Duration(seconds: 2 + delaySec + 1), () {
@@ -1854,8 +1857,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
player: player!, player: player!,
availableVersions: _availableVersions, availableVersions: _availableVersions,
selectedMediaIndex: widget.selectedMediaIndex, selectedMediaIndex: widget.selectedMediaIndex,
initialBoxFitMode: settings.getDefaultBoxFitMode(), initialBoxFitMode: settings.read(SettingsService.defaultBoxFitMode),
onBoxFitModeChanged: (mode) => settings.setDefaultBoxFitMode(mode), onBoxFitModeChanged: (mode) => settings.write(SettingsService.defaultBoxFitMode, mode),
); );
_videoFilterManager!.updateVideoFilter(); _videoFilterManager!.updateVideoFilter();
@@ -1902,7 +1905,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
try { try {
final shaderProvider = context.read<ShaderProvider>(); final shaderProvider = context.read<ShaderProvider>();
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final presetId = settings.getGlobalShaderPreset(); final presetId = settings.read(SettingsService.globalShaderPreset);
final preset = final preset =
(shaderProvider.initialized ? shaderProvider.findPresetById(presetId) : ShaderPreset.fromId(presetId)) ?? (shaderProvider.initialized ? shaderProvider.findPresetById(presetId) : ShaderPreset.fromId(presetId)) ??
ShaderPreset.none; ShaderPreset.none;
@@ -1918,7 +1921,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Future<void> _restoreAmbientLighting() async { Future<void> _restoreAmbientLighting() async {
final shaderProvider = context.read<ShaderProvider>(); final shaderProvider = context.read<ShaderProvider>();
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (!settings.getAmbientLighting()) return; if (!settings.read(SettingsService.ambientLighting)) return;
final ambientLighting = _ambientLightingService; final ambientLighting = _ambientLightingService;
if (ambientLighting == null || !ambientLighting.isSupported) return; if (ambientLighting == null || !ambientLighting.isSupported) return;
@@ -2005,7 +2008,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Persist ambient lighting state // Persist ambient lighting state
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
settings.setAmbientLighting(ambientLighting.isEnabled); settings.write(SettingsService.ambientLighting, ambientLighting.isEnabled);
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
@@ -2123,7 +2126,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
receiver.onSeekForward = () async { receiver.onSeekForward = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final seekSeconds = settings.getSeekTimeSmall(); final seekSeconds = settings.read(SettingsService.seekTimeSmall);
if (widget.isLive && _captureBuffer != null) { if (widget.isLive && _captureBuffer != null) {
await _seekLivePosition(_currentPositionEpoch + seekSeconds); await _seekLivePosition(_currentPositionEpoch + seekSeconds);
return; return;
@@ -2134,7 +2137,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
receiver.onSeekBackward = () async { receiver.onSeekBackward = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final seekSeconds = settings.getSeekTimeSmall(); final seekSeconds = settings.read(SettingsService.seekTimeSmall);
if (widget.isLive && _captureBuffer != null) { if (widget.isLive && _captureBuffer != null) {
await _seekLivePosition(_currentPositionEpoch - seekSeconds); await _seekLivePosition(_currentPositionEpoch - seekSeconds);
return; return;
@@ -2145,25 +2148,25 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
receiver.onVolumeUp = () async { receiver.onVolumeUp = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); 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); final newVolume = (player!.state.volume + 10).clamp(0.0, maxVol);
player!.setVolume(newVolume); player!.setVolume(newVolume);
settings.setVolume(newVolume); settings.write(SettingsService.volume, newVolume);
}; };
receiver.onVolumeDown = () async { receiver.onVolumeDown = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); 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); final newVolume = (player!.state.volume - 10).clamp(0.0, maxVol);
player!.setVolume(newVolume); player!.setVolume(newVolume);
settings.setVolume(newVolume); settings.write(SettingsService.volume, newVolume);
}; };
receiver.onVolumeMute = () async { receiver.onVolumeMute = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final newVolume = player!.state.volume > 0 ? 0.0 : 100.0; final newVolume = player!.state.volume > 0 ? 0.0 : 100.0;
player!.setVolume(newVolume); player!.setVolume(newVolume);
settings.setVolume(newVolume); settings.write(SettingsService.volume, newVolume);
}; };
receiver.onSubtitles = _cycleSubtitleTrack; receiver.onSubtitles = _cycleSubtitleTrack;
receiver.onAudioTracks = _cycleAudioTrack; receiver.onAudioTracks = _cycleAudioTrack;
@@ -2489,7 +2492,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final isKeyboardMode = PlatformDetector.isTV() && InputModeTracker.isKeyboardMode(context); final isKeyboardMode = PlatformDetector.isTV() && InputModeTracker.isKeyboardMode(context);
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final autoPlayEnabled = settings.getAutoPlayNextEpisode(); final autoPlayEnabled = settings.read(SettingsService.autoPlayNextEpisode);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.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. /// 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 /// 3. Optionally override onInit() for post-initialization setup
abstract class BaseSharedPreferencesService { abstract class BaseSharedPreferencesService {
static final Map<Type, BaseSharedPreferencesService> _instances = {}; static final Map<Type, BaseSharedPreferencesService> _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<SharedPreferencesWithCache>? _cacheFuture;
late SharedPreferencesWithCache _cache;
/// Protected constructor for subclasses /// Protected constructor for subclasses
BaseSharedPreferencesService(); BaseSharedPreferencesService();
/// Access to SharedPreferences instance /// Access to the underlying preferences cache.
SharedPreferences get prefs => _prefs; SharedPreferencesWithCache get prefs => _cache;
/// Initialize the SharedPreferences instance /// Initialize the preferences instance.
/// ///
/// This method handles: /// This method handles:
/// - Singleton instance management /// - 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 /// - Calling onInit() hook for subclass-specific setup
static Future<T> initializeInstance<T extends BaseSharedPreferencesService>(T Function() constructor) async { static Future<T> initializeInstance<T extends BaseSharedPreferencesService>(T Function() constructor) async {
if (_instances[T] == null) { if (_instances[T] == null) {
final instance = constructor(); final instance = constructor();
_instances[T] = instance; _instances[T] = instance;
instance._prefs = await SharedPreferences.getInstance(); instance._cache = await _ensureCache();
await instance.onInit(); await instance.onInit();
} }
return _instances[T] as T; return _instances[T] as T;
} }
static Future<SharedPreferencesWithCache> _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<String> readStringList(String key, {List<String> 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<void> writeBool(String key, bool value) => _cache.setBool(key, value);
Future<void> writeInt(String key, int value) => _cache.setInt(key, value);
Future<void> writeDouble(String key, double value) => _cache.setDouble(key, value);
Future<void> writeString(String key, String value) => _cache.setString(key, value);
Future<void> writeStringList(String key, List<String> value) => _cache.setStringList(key, value);
/// Decode a JSON string to a Map with error handling. /// Decode a JSON string to a Map with error handling.
/// ///
/// If [legacyStringOk] is true and the value is a plain string (not valid /// 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<T>(Pref<T> 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<void> write<T>(Pref<T> pref, T value) async {
await pref.writeTo(this, value);
final n = _listenables[pref.key];
if (n != null) (n as ValueNotifier<T>).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<String, ValueNotifier<dynamic>> _listenables = {};
ValueNotifier<T> listenable<T>(Pref<T> pref) {
return (_listenables[pref.key] ??= ValueNotifier<T>(read(pref))) as ValueNotifier<T>;
}
/// Hook for subclass-specific initialization after SharedPreferences is ready. /// Hook for subclass-specific initialization after SharedPreferences is ready.
/// ///
/// Override this method to perform any setup that requires access to /// Override this method to perform any setup that requires access to
@@ -58,3 +113,136 @@ abstract class BaseSharedPreferencesService {
// Default implementation does nothing // Default implementation does nothing
} }
} }
/// Typed preference declaration. Pair with [BaseSharedPreferencesService.read]
/// and [BaseSharedPreferencesService.write] to remove per-setting `?? default`
/// boilerplate.
abstract class Pref<T> {
final String key;
const Pref(this.key);
/// Implementation hook — call [BaseSharedPreferencesService.read] instead.
T readFrom(BaseSharedPreferencesService svc);
/// Implementation hook — call [BaseSharedPreferencesService.write] instead.
Future<void> writeTo(BaseSharedPreferencesService svc, T value);
}
class BoolPref extends Pref<bool> {
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<void> writeTo(BaseSharedPreferencesService svc, bool value) async {
await svc.writeBool(key, value);
onWrite?.call(value);
}
}
class IntPref extends Pref<int> {
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<void> writeTo(BaseSharedPreferencesService svc, int value) =>
svc.writeInt(key, transform == null ? value : transform!(value));
}
class DoublePref extends Pref<double> {
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<void> writeTo(BaseSharedPreferencesService svc, double value) =>
svc.writeDouble(key, transform == null ? value : transform!(value));
}
class StringPref extends Pref<String> {
final String defaultValue;
const StringPref(super.key, {this.defaultValue = ''});
@override
String readFrom(BaseSharedPreferencesService svc) => svc.readString(key, defaultValue: defaultValue);
@override
Future<void> 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<String?> {
final String? Function(String?)? transform;
const NullableStringPref(super.key, {this.transform});
@override
String? readFrom(BaseSharedPreferencesService svc) => svc.prefs.getString(key);
@override
Future<void> 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<List<String>> {
final List<String> defaultValue;
const StringListPref(super.key, {this.defaultValue = const []});
@override
List<String> readFrom(BaseSharedPreferencesService svc) => svc.readStringList(key, defaultValue: defaultValue);
@override
Future<void> writeTo(BaseSharedPreferencesService svc, List<String> 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<T extends Enum> extends Pref<T> {
final List<T> 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<void> 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<T> extends Pref<T> {
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<void> writeTo(BaseSharedPreferencesService svc, T value) => svc.writeString(key, encode(value));
}
+1 -1
View File
@@ -76,7 +76,7 @@ class DiscordRPCService {
_isInitialized = true; _isInitialized = true;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
_isEnabled = settings.getEnableDiscordRPC(); _isEnabled = settings.read(SettingsService.enableDiscordRPC);
if (_isEnabled) { if (_isEnabled) {
await _connect(); await _connect();
+3 -3
View File
@@ -34,7 +34,7 @@ class DisplayModeService {
bool anyChange = false; bool anyChange = false;
// Refresh rate matching. // Refresh rate matching.
if (_settings.getMatchRefreshRate() && fps != null && fps > 0) { if (_settings.read(SettingsService.matchRefreshRate) && fps != null && fps > 0) {
try { try {
final success = await _matchRefreshRate(fps); final success = await _matchRefreshRate(fps);
anyChange |= success; anyChange |= success;
@@ -44,7 +44,7 @@ class DisplayModeService {
} }
// Dynamic range matching. // Dynamic range matching.
if (_settings.getMatchDynamicRange() && sigPeak != null && sigPeak > 1.0) { if (_settings.read(SettingsService.matchDynamicRange) && sigPeak != null && sigPeak > 1.0) {
try { try {
final success = await _enableSystemHDR(); final success = await _enableSystemHDR();
anyChange |= success; anyChange |= success;
@@ -54,7 +54,7 @@ class DisplayModeService {
} }
if (anyChange) { if (anyChange) {
final delaySec = _settings.getDisplaySwitchDelay(); final delaySec = _settings.read(SettingsService.displaySwitchDelay);
return Duration(seconds: delaySec); return Duration(seconds: delaySec);
} }
+2 -2
View File
@@ -125,7 +125,7 @@ class DownloadManagerService {
/// pay for a second platform round-trip. /// pay for a second platform round-trip.
static Future<bool> shouldBlockDownloadOnCellularWith(List<ConnectivityResult> connectivity) async { static Future<bool> shouldBlockDownloadOnCellularWith(List<ConnectivityResult> connectivity) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (!settings.getDownloadOnWifiOnly()) return false; if (!settings.read(SettingsService.downloadOnWifiOnly)) return false;
if (connectivity.isEmpty) return false; if (connectivity.isEmpty) return false;
return connectivity.contains(ConnectivityResult.mobile) && return connectivity.contains(ConnectivityResult.mobile) &&
!connectivity.contains(ConnectivityResult.wifi) && !connectivity.contains(ConnectivityResult.wifi) &&
@@ -610,7 +610,7 @@ class DownloadManagerService {
// Get WiFi-only setting for native enforcement // Get WiFi-only setting for native enforcement
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final requiresWiFi = settings.getDownloadOnWifiOnly(); final requiresWiFi = settings.read(SettingsService.downloadOnWifiOnly);
if (_storageService.isUsingSaf) { if (_storageService.isUsingSaf) {
// SAF mode: use UriDownloadTask (writes directly to content:// URI, no pause/resume) // SAF mode: use UriDownloadTask (writes directly to content:// URI, no pause/resume)
+4 -4
View File
@@ -47,8 +47,8 @@ class DownloadStorageService {
/// Initialize with settings service (call during app startup) /// Initialize with settings service (call during app startup)
Future<void> initialize(SettingsService settingsService) async { Future<void> initialize(SettingsService settingsService) async {
_settingsService = settingsService; _settingsService = settingsService;
_customDownloadPath = settingsService.getCustomDownloadPath(); _customDownloadPath = settingsService.read(SettingsService.customDownloadPath);
_customPathType = settingsService.getCustomDownloadPathType(); _customPathType = settingsService.read(SettingsService.customDownloadPathType) ?? 'file';
// Reset cached directories to force recalculation // Reset cached directories to force recalculation
_baseDownloadsDir = null; _baseDownloadsDir = null;
_artworkDirectoryPath = null; _artworkDirectoryPath = null;
@@ -60,8 +60,8 @@ class DownloadStorageService {
/// Refresh custom path from settings (call when settings change) /// Refresh custom path from settings (call when settings change)
Future<void> refreshCustomPath() async { Future<void> refreshCustomPath() async {
if (_settingsService != null) { if (_settingsService != null) {
_customDownloadPath = _settingsService!.getCustomDownloadPath(); _customDownloadPath = _settingsService!.read(SettingsService.customDownloadPath);
_customPathType = _settingsService!.getCustomDownloadPathType(); _customPathType = _settingsService!.read(SettingsService.customDownloadPathType) ?? 'file';
_baseDownloadsDir = null; _baseDownloadsDir = null;
_artworkDirectoryPath = null; _artworkDirectoryPath = null;
await getArtworkDirectory(); await getArtworkDirectory();
+1 -1
View File
@@ -44,7 +44,7 @@ class ExternalPlayerService {
} }
final settings = await SettingsService.getInstance(); 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 // On Android, always use native intent to avoid url_launcher opening in browser
if (Platform.isAndroid && context.mounted) { if (Platform.isAndroid && context.mounted) {
+23 -20
View File
@@ -38,11 +38,11 @@ class KeyboardShortcutsService {
_settingsService = await SettingsService.getInstance(); _settingsService = await SettingsService.getInstance();
// Ensure settings service is fully initialized before loading data // Ensure settings service is fully initialized before loading data
await Future.delayed(Duration.zero); // Allow event loop to complete await Future.delayed(Duration.zero); // Allow event loop to complete
_shortcuts = _settingsService.getKeyboardShortcuts(); // Keep for legacy compatibility _shortcuts = _settingsService.read(SettingsService.keyboardShortcuts); // Keep for legacy compatibility
_hotkeys = await _settingsService.getKeyboardHotkeys(); // Primary method _hotkeys = _settingsService.read(SettingsService.keyboardHotkeys); // Primary method
_seekTimeSmall = _settingsService.getSeekTimeSmall(); _seekTimeSmall = _settingsService.read(SettingsService.seekTimeSmall);
_seekTimeLarge = _settingsService.getSeekTimeLarge(); _seekTimeLarge = _settingsService.read(SettingsService.seekTimeLarge);
_maxVolume = _settingsService.getMaxVolume(); _maxVolume = _settingsService.read(SettingsService.maxVolume);
} }
Map<String, String> get shortcuts => Map.from(_shortcuts); Map<String, String> get shortcuts => Map.from(_shortcuts);
@@ -59,7 +59,7 @@ class KeyboardShortcutsService {
Future<void> setShortcut(String action, String key) async { Future<void> setShortcut(String action, String key) async {
_shortcuts[action] = key; _shortcuts[action] = key;
await _settingsService.setKeyboardShortcuts(_shortcuts); await _settingsService.write(SettingsService.keyboardShortcuts, _shortcuts);
} }
Future<void> setHotkey(String action, HotKey hotkey) async { Future<void> setHotkey(String action, HotKey hotkey) async {
@@ -67,7 +67,10 @@ class KeyboardShortcutsService {
_hotkeys[action] = hotkey; _hotkeys[action] = hotkey;
// Save to persistent storage // 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 // Verify local cache is still correct
if (_hotkeys[action] != hotkey) { if (_hotkeys[action] != hotkey) {
@@ -76,16 +79,16 @@ class KeyboardShortcutsService {
} }
Future<void> refreshFromStorage() async { Future<void> refreshFromStorage() async {
_hotkeys = await _settingsService.getKeyboardHotkeys(); _hotkeys = _settingsService.read(SettingsService.keyboardHotkeys);
_seekTimeSmall = _settingsService.getSeekTimeSmall(); _seekTimeSmall = _settingsService.read(SettingsService.seekTimeSmall);
_seekTimeLarge = _settingsService.getSeekTimeLarge(); _seekTimeLarge = _settingsService.read(SettingsService.seekTimeLarge);
} }
Future<void> resetToDefaults() async { Future<void> resetToDefaults() async {
_shortcuts = _settingsService.getDefaultKeyboardShortcuts(); _shortcuts = SettingsService.defaultKeyboardShortcuts();
_hotkeys = _settingsService.getDefaultKeyboardHotkeys(); _hotkeys = SettingsService.defaultKeyboardHotkeys();
await _settingsService.setKeyboardShortcuts(_shortcuts); await _settingsService.write(SettingsService.keyboardShortcuts, _shortcuts);
await _settingsService.setKeyboardHotkeys(_hotkeys); await _settingsService.write(SettingsService.keyboardHotkeys, _hotkeys);
// Refresh cache to ensure consistency // Refresh cache to ensure consistency
await refreshFromStorage(); await refreshFromStorage();
} }
@@ -265,12 +268,12 @@ class KeyboardShortcutsService {
case 'volume_up': case 'volume_up':
final newVolume = (player.state.volume + 10).clamp(0.0, _maxVolume.toDouble()); final newVolume = (player.state.volume + 10).clamp(0.0, _maxVolume.toDouble());
player.setVolume(newVolume); player.setVolume(newVolume);
_settingsService.setVolume(newVolume); _settingsService.write(SettingsService.volume, newVolume);
break; break;
case 'volume_down': case 'volume_down':
final newVolume = (player.state.volume - 10).clamp(0.0, _maxVolume.toDouble()); final newVolume = (player.state.volume - 10).clamp(0.0, _maxVolume.toDouble());
player.setVolume(newVolume); player.setVolume(newVolume);
_settingsService.setVolume(newVolume); _settingsService.write(SettingsService.volume, newVolume);
break; break;
case 'seek_forward': case 'seek_forward':
performSeek(_seekTimeSmall); performSeek(_seekTimeSmall);
@@ -290,7 +293,7 @@ class KeyboardShortcutsService {
case 'mute_toggle': case 'mute_toggle':
final newVolume = player.state.volume > 0 ? 0.0 : 100.0; final newVolume = player.state.volume > 0 ? 0.0 : 100.0;
player.setVolume(newVolume); player.setVolume(newVolume);
_settingsService.setVolume(newVolume); _settingsService.write(SettingsService.volume, newVolume);
break; break;
case 'subtitle_toggle': case 'subtitle_toggle':
onToggleSubtitles?.call(); onToggleSubtitles?.call();
@@ -316,16 +319,16 @@ class KeyboardShortcutsService {
case 'speed_increase': case 'speed_increase':
final newRateUp = (player.state.rate + 0.25).clamp(0.25, 3.0); final newRateUp = (player.state.rate + 0.25).clamp(0.25, 3.0);
player.setRate(newRateUp); player.setRate(newRateUp);
_settingsService.setDefaultPlaybackSpeed(newRateUp); _settingsService.write(SettingsService.defaultPlaybackSpeed, newRateUp);
break; break;
case 'speed_decrease': case 'speed_decrease':
final newRateDown = (player.state.rate - 0.25).clamp(0.25, 3.0); final newRateDown = (player.state.rate - 0.25).clamp(0.25, 3.0);
player.setRate(newRateDown); player.setRate(newRateDown);
_settingsService.setDefaultPlaybackSpeed(newRateDown); _settingsService.write(SettingsService.defaultPlaybackSpeed, newRateDown);
break; break;
case 'speed_reset': case 'speed_reset':
player.setRate(1.0); player.setRate(1.0);
_settingsService.setDefaultPlaybackSpeed(1.0); _settingsService.write(SettingsService.defaultPlaybackSpeed, 1.0);
break; break;
case 'sub_seek_next': case 'sub_seek_next':
player.command(['sub-seek', '1']); player.command(['sub-seek', '1']);
+1 -1
View File
@@ -242,7 +242,7 @@ class MultiServerManager {
.fetchServerPrefs() .fetchServerPrefs()
.then((_) { .then((_) {
final threshold = client.watchedThresholdPercent; final threshold = client.watchedThresholdPercent;
SettingsService.instanceOrNull?.setWatchedThreshold(serverId, threshold); SettingsService.instanceOrNull?.write(SettingsService.watchedThresholdPref(serverId), threshold);
}) })
.catchError((Object e, StackTrace st) { .catchError((Object e, StackTrace st) {
appLogger.w('fetchServerPrefs failed for ${server.name}', error: e, stackTrace: st); appLogger.w('fetchServerPrefs failed for ${server.name}', error: e, stackTrace: st);
+1 -1
View File
@@ -44,7 +44,7 @@ class OfflineWatchSyncService extends ChangeNotifier {
if (client != null && client.serverPrefs.isNotEmpty) { if (client != null && client.serverPrefs.isNotEmpty) {
return client.watchedThresholdPercent / 100.0; 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; return cached / 100.0;
} }
+7 -6
View File
@@ -11,6 +11,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/platform_detector.dart'; import '../utils/platform_detector.dart';
import 'file_picker_service.dart'; import 'file_picker_service.dart';
import 'settings_service.dart';
import 'storage_service.dart'; import 'storage_service.dart';
class ImportResult { class ImportResult {
@@ -101,7 +102,7 @@ class SettingsExportService {
/// prefix stripped so they can be re-scoped on import. Keys belonging to any /// 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). /// OTHER user are skipped (we only export the active user's prefs).
static Map<String, dynamic> buildExportMap( static Map<String, dynamic> buildExportMap(
SharedPreferences prefs, { SharedPreferencesWithCache prefs, {
String? currentUserUuid, String? currentUserUuid,
String appVersion = '', String appVersion = '',
}) { }) {
@@ -110,7 +111,7 @@ class SettingsExportService {
? '$_userPrefixRoot${currentUserUuid}_' ? '$_userPrefixRoot${currentUserUuid}_'
: null; : null;
for (final fullKey in prefs.getKeys()) { for (final fullKey in prefs.keys) {
String baseKey; String baseKey;
if (currentUserPrefix != null && fullKey.startsWith(currentUserPrefix)) { if (currentUserPrefix != null && fullKey.startsWith(currentUserPrefix)) {
baseKey = fullKey.substring(currentUserPrefix.length); baseKey = fullKey.substring(currentUserPrefix.length);
@@ -159,7 +160,7 @@ class SettingsExportService {
/// Throws [SettingsExportException] for structural problems. /// Throws [SettingsExportException] for structural problems.
static Future<ImportResult> applyImportMap( static Future<ImportResult> applyImportMap(
Map<String, dynamic> data, Map<String, dynamic> data,
SharedPreferences prefs, { SharedPreferencesWithCache prefs, {
required String currentUserUuid, required String currentUserUuid,
}) async { }) async {
final version = data['formatVersion']; final version = data['formatVersion'];
@@ -222,7 +223,7 @@ class SettingsExportService {
return prefixes.any(baseKey.startsWith); return prefixes.any(baseKey.startsWith);
} }
static Future<bool> _writeTyped(SharedPreferences prefs, String key, String type, Object? value) async { static Future<bool> _writeTyped(SharedPreferencesWithCache prefs, String key, String type, Object? value) async {
try { try {
switch (type) { switch (type) {
case _typeBool: case _typeBool:
@@ -270,7 +271,7 @@ class SettingsExportService {
/// ///
/// Throws [SettingsExportException] on failure. /// Throws [SettingsExportException] on failure.
static Future<String?> exportToFile() async { static Future<String?> exportToFile() async {
final prefs = await SharedPreferences.getInstance(); final prefs = (await SettingsService.getInstance()).prefs;
final storage = await StorageService.getInstance(); final storage = await StorageService.getInstance();
String appVersion = ''; String appVersion = '';
try { try {
@@ -359,7 +360,7 @@ class SettingsExportService {
throw const InvalidExportFileException('Invalid export file'); throw const InvalidExportFileException('Invalid export file');
} }
final prefs = await SharedPreferences.getInstance(); final prefs = (await SettingsService.getInstance()).prefs;
return applyImportMap(data, prefs, currentUserUuid: uuid); return applyImportMap(data, prefs, currentUserUuid: uuid);
} }
} }
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -368,7 +368,7 @@ class StorageService extends BaseSharedPreferencesService {
/// Load all persisted episode counts /// Load all persisted episode counts
Map<String, int> loadAllEpisodeCounts() { Map<String, int> loadAllEpisodeCounts() {
final counts = <String, int>{}; final counts = <String, int>{};
final keys = prefs.getKeys().where((k) => k.startsWith(_prefixEpisodeCount)); final keys = prefs.keys.where((k) => k.startsWith(_prefixEpisodeCount));
for (final key in keys) { for (final key in keys) {
final globalKey = key.replaceFirst(_prefixEpisodeCount, ''); final globalKey = key.replaceFirst(_prefixEpisodeCount, '');
@@ -415,7 +415,7 @@ class StorageService extends BaseSharedPreferencesService {
/// Remove all keys matching a prefix /// Remove all keys matching a prefix
Future<void> _clearKeysWithPrefix(String prefix) async { Future<void> _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))); await Future.wait(keys.map((k) => prefs.remove(k)));
} }
+2 -2
View File
@@ -180,7 +180,7 @@ class TrackManager {
preferredAudioTrack: preferredAudioTrack, preferredAudioTrack: preferredAudioTrack,
preferredSubtitleTrack: preferredSubtitleTrack, preferredSubtitleTrack: preferredSubtitleTrack,
preferredSecondarySubtitleTrack: preferredSecondarySubtitleTrack, preferredSecondarySubtitleTrack: preferredSecondarySubtitleTrack,
defaultPlaybackSpeed: settingsService.getDefaultPlaybackSpeed(), defaultPlaybackSpeed: settingsService.read(SettingsService.defaultPlaybackSpeed),
onAudioTrackChanged: onAudioTrackChanged, onAudioTrackChanged: onAudioTrackChanged,
onSubtitleTrackChanged: onSubtitleTrackChanged, onSubtitleTrackChanged: onSubtitleTrackChanged,
); );
@@ -354,7 +354,7 @@ class TrackManager {
/// Common guard checks for track change handlers. /// Common guard checks for track change handlers.
Future<int?> _guardTrackChange(PlexMediaInfo? info) async { Future<int?> _guardTrackChange(PlexMediaInfo? info) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (!settings.getRememberTrackSelections()) return null; if (!settings.read(SettingsService.rememberTrackSelections)) return null;
if (info == null) { if (info == null) {
appLogger.w('No media info available, cannot save stream selection'); appLogger.w('No media info available, cannot save stream selection');
@@ -30,7 +30,7 @@ class AnilistTracker extends TrackerBase {
bool get hasActiveClient => _client != null; bool get hasActiveClient => _client != null;
@override @override
bool readEnabledSetting(SettingsService settings) => settings.getEnableAnilistScrobble(); bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableAnilistScrobble);
void rebindSession(AnilistSession? session, {required void Function() onSessionInvalidated}) { void rebindSession(AnilistSession? session, {required void Function() onSessionInvalidated}) {
_client?.dispose(); _client?.dispose();
+1 -1
View File
@@ -35,7 +35,7 @@ class MalTracker extends TrackerBase {
bool get hasActiveClient => _client != null; bool get hasActiveClient => _client != null;
@override @override
bool readEnabledSetting(SettingsService settings) => settings.getEnableMalScrobble(); bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableMalScrobble);
void rebindSession( void rebindSession(
MalSession? session, { MalSession? session, {
@@ -33,7 +33,7 @@ class SimklTracker extends TrackerBase {
bool get hasActiveClient => _client != null; bool get hasActiveClient => _client != null;
@override @override
bool readEnabledSetting(SettingsService settings) => settings.getEnableSimklScrobble(); bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableSimklScrobble);
void rebindSession(SimklSession? session, {required void Function() onSessionInvalidated}) { void rebindSession(SimklSession? session, {required void Function() onSessionInvalidated}) {
_client?.dispose(); _client?.dispose();
@@ -55,7 +55,7 @@ class TraktScrobbleService {
if (_isInitialized) return; if (_isInitialized) return;
_isInitialized = true; _isInitialized = true;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
_isEnabled = settings.getEnableTraktScrobble(); _isEnabled = settings.read(SettingsService.enableTraktScrobble);
} }
Future<void> setEnabled(bool enabled) async { Future<void> setEnabled(bool enabled) async {
+1 -1
View File
@@ -56,7 +56,7 @@ class TraktSyncService {
_serverManager = serverManager; _serverManager = serverManager;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
_isEnabled = settings.getEnableTraktWatchedSync(); _isEnabled = settings.read(SettingsService.enableTraktWatchedSync);
_subscription = WatchStateNotifier().stream.listen( _subscription = WatchStateNotifier().stream.listen(
_onWatchStateEvent, _onWatchStateEvent,
+2 -2
View File
@@ -70,7 +70,7 @@ Future<bool?> navigateToVideoPlayer(
try { try {
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
final seriesKey = metadata.grandparentRatingKey ?? metadata.ratingKey; final seriesKey = metadata.grandparentRatingKey ?? metadata.ratingKey;
final savedPreference = settingsService.getMediaVersionPreference(seriesKey); final savedPreference = settingsService.read(SettingsService.mediaVersionPreferences)[seriesKey];
if (savedPreference != null) { if (savedPreference != null) {
mediaIndex = savedPreference; mediaIndex = savedPreference;
// Pre-parsed playbackData was built with mediaIndex=0; invalidate if // Pre-parsed playbackData was built with mediaIndex=0; invalidate if
@@ -87,7 +87,7 @@ Future<bool?> navigateToVideoPlayer(
// Check if external player is enabled // Check if external player is enabled
try { try {
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
if (settingsService.getUseExternalPlayer()) { if (settingsService.read(SettingsService.useExternalPlayer)) {
bool launched = false; bool launched = false;
if (isOffline) { if (isOffline) {
@@ -210,7 +210,7 @@ class WatchTogetherProvider with ChangeNotifier {
appLogger.d('WatchTogether: Creating session with control mode: $controlMode'); 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); _peerService = WatchTogetherPeerService(customBaseUrl: customRelayUrl);
_setupPeerServiceListeners(); _setupPeerServiceListeners();
@@ -258,7 +258,7 @@ class WatchTogetherProvider with ChangeNotifier {
appLogger.d('WatchTogether: Joining session: $sessionId'); appLogger.d('WatchTogether: Joining session: $sessionId');
final customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); final customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl);
_peerService = WatchTogetherPeerService(customBaseUrl: customRelayUrl); _peerService = WatchTogetherPeerService(customBaseUrl: customRelayUrl);
_setupPeerServiceListeners(); _setupPeerServiceListeners();
@@ -312,7 +312,7 @@ class WatchTogetherProvider with ChangeNotifier {
// Probe the relay with a lightweight peer service to check room occupancy, // Probe the relay with a lightweight peer service to check room occupancy,
// then do a single createSession or joinSession. This avoids the crash-prone // then do a single createSession or joinSession. This avoids the crash-prone
// join→teardown→create cycle on the provider. // join→teardown→create cycle on the provider.
final customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); final customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl);
final probe = WatchTogetherPeerService(customBaseUrl: customRelayUrl); final probe = WatchTogetherPeerService(customBaseUrl: customRelayUrl);
bool shouldBeHost; bool shouldBeHost;
try { try {
@@ -91,7 +91,7 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl(); _customRelayUrl = SettingsService.instanceOrNull?.read(SettingsService.customRelayUrl);
_recentRooms = RecentRoomsService.getRecentRooms(); _recentRooms = RecentRoomsService.getRecentRooms();
_checkHealth(); _checkHealth();
} }
@@ -46,7 +46,7 @@ class RecentRoomsService {
static const int _maxRooms = 20; static const int _maxRooms = 20;
static List<RecentRoom> getRecentRooms() { static List<RecentRoom> getRecentRooms() {
final json = SettingsService.instanceOrNull?.getRecentRooms(); final json = SettingsService.instanceOrNull?.read(SettingsService.recentRooms);
if (json == null) return []; if (json == null) return [];
try { try {
final list = jsonDecode(json) as List<dynamic>; final list = jsonDecode(json) as List<dynamic>;
@@ -61,7 +61,10 @@ class RecentRoomsService {
static Future<void> _save(List<RecentRoom> rooms) async { static Future<void> _save(List<RecentRoom> rooms) async {
rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed)); rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed));
if (rooms.length > _maxRooms) rooms.removeRange(_maxRooms, rooms.length); 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<void> addOrUpdateRoom(String code, {String? name, ControlMode? controlMode}) async { static Future<void> addOrUpdateRoom(String code, {String? name, ControlMode? controlMode}) async {
@@ -150,18 +150,18 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_enableHDR = settings.getEnableHDR(); _enableHDR = settings.read(SettingsService.enableHDR);
_showPerformanceOverlay = settings.getShowPerformanceOverlay(); _showPerformanceOverlay = settings.read(SettingsService.showPerformanceOverlay);
_autoPlayNextEpisode = settings.getAutoPlayNextEpisode(); _autoPlayNextEpisode = settings.read(SettingsService.autoPlayNextEpisode);
_audioPassthrough = settings.getAudioPassthrough(); _audioPassthrough = settings.read(SettingsService.audioPassthrough);
_audioNormalization = settings.getAudioNormalization(); _audioNormalization = settings.read(SettingsService.audioNormalization);
}); });
} }
Future<void> _toggleHDR() async { Future<void> _toggleHDR() async {
final newValue = !_enableHDR; final newValue = !_enableHDR;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setEnableHDR(newValue); await settings.write(SettingsService.enableHDR, newValue);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_enableHDR = newValue; _enableHDR = newValue;
@@ -173,7 +173,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Future<void> _togglePerformanceOverlay() async { Future<void> _togglePerformanceOverlay() async {
final newValue = !_showPerformanceOverlay; final newValue = !_showPerformanceOverlay;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setShowPerformanceOverlay(newValue); await settings.write(SettingsService.showPerformanceOverlay, newValue);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_showPerformanceOverlay = newValue; _showPerformanceOverlay = newValue;
@@ -183,7 +183,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Future<void> _toggleAutoPlayNextEpisode() async { Future<void> _toggleAutoPlayNextEpisode() async {
final newValue = !_autoPlayNextEpisode; final newValue = !_autoPlayNextEpisode;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setAutoPlayNextEpisode(newValue); await settings.write(SettingsService.autoPlayNextEpisode, newValue);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_autoPlayNextEpisode = newValue; _autoPlayNextEpisode = newValue;
@@ -193,7 +193,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Future<void> _toggleAudioPassthrough() async { Future<void> _toggleAudioPassthrough() async {
final newValue = !_audioPassthrough; final newValue = !_audioPassthrough;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setAudioPassthrough(newValue); await settings.write(SettingsService.audioPassthrough, newValue);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_audioPassthrough = newValue; _audioPassthrough = newValue;
@@ -204,7 +204,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Future<void> _toggleAudioNormalization() async { Future<void> _toggleAudioNormalization() async {
final newValue = !_audioNormalization; final newValue = !_audioNormalization;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setAudioNormalization(newValue); await settings.write(SettingsService.audioNormalization, newValue);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_audioNormalization = newValue; _audioNormalization = newValue;
@@ -256,9 +256,9 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
onOffsetChanged: (offset) async { onOffsetChanged: (offset) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (isSubtitle) { if (isSubtitle) {
await settings.setSubtitleSyncOffset(offset); await settings.write(SettingsService.subtitleSyncOffset, offset);
} else { } else {
await settings.setAudioSyncOffset(offset); await settings.write(SettingsService.audioSyncOffset, offset);
} }
widget.onSyncOffsetChanged?.call(propertyName, offset); widget.onSyncOffsetChanged?.call(propertyName, offset);
}, },
@@ -562,7 +562,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
widget.player.setRate(speed); widget.player.setRate(speed);
// Save as default playback speed // Save as default playback speed
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setDefaultPlaybackSpeed(speed); await settings.write(SettingsService.defaultPlaybackSpeed, speed);
if (context.mounted) { if (context.mounted) {
OverlaySheetController.of(context).close(); // Close sheet after selection OverlaySheetController.of(context).close(); // Close sheet after selection
} }
+22 -19
View File
@@ -647,18 +647,18 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
if (mounted) { if (mounted) {
setState(() { setState(() {
_seekTimeSmall = settingsService.getSeekTimeSmall(); _seekTimeSmall = settingsService.read(SettingsService.seekTimeSmall);
_rewindOnResume = settingsService.getRewindOnResume(); _rewindOnResume = settingsService.read(SettingsService.rewindOnResume);
_audioSyncOffset = settingsService.getAudioSyncOffset(); _audioSyncOffset = settingsService.read(SettingsService.audioSyncOffset);
_subtitleSyncOffset = settingsService.getSubtitleSyncOffset(); _subtitleSyncOffset = settingsService.read(SettingsService.subtitleSyncOffset);
_isRotationLocked = settingsService.getRotationLocked(); _isRotationLocked = settingsService.read(SettingsService.rotationLocked);
_autoSkipIntro = settingsService.getAutoSkipIntro(); _autoSkipIntro = settingsService.read(SettingsService.autoSkipIntro);
_autoSkipCredits = settingsService.getAutoSkipCredits(); _autoSkipCredits = settingsService.read(SettingsService.autoSkipCredits);
_autoSkipDelay = settingsService.getAutoSkipDelay(); _autoSkipDelay = settingsService.read(SettingsService.autoSkipDelay);
_videoPlayerNavigationEnabled = settingsService.getVideoPlayerNavigationEnabled(); _videoPlayerNavigationEnabled = settingsService.read(SettingsService.videoPlayerNavigationEnabled);
_showPerformanceOverlay = settingsService.getShowPerformanceOverlay(); _showPerformanceOverlay = settingsService.read(SettingsService.showPerformanceOverlay);
_autoHidePerformanceOverlay = settingsService.getAutoHidePerformanceOverlay(); _autoHidePerformanceOverlay = settingsService.read(SettingsService.autoHidePerformanceOverlay);
_clickVideoTogglesPlayback = settingsService.getClickVideoTogglesPlayback(); _clickVideoTogglesPlayback = settingsService.read(SettingsService.clickVideoTogglesPlayback);
}); });
// Focus play/pause if navigation is now enabled and controls are visible // Focus play/pause if navigation is now enabled and controls are visible
@@ -940,7 +940,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
final maxVol = _keyboardService!.maxVolume.toDouble(); final maxVol = _keyboardService!.maxVolume.toDouble();
final newVolume = (volume - delta / 20).clamp(0.0, maxVol); final newVolume = (volume - delta / 20).clamp(0.0, maxVol);
widget.player.setVolume(newVolume); widget.player.setVolume(newVolume);
unawaited(SettingsService.getInstance().then((s) => s.setVolume(newVolume))); unawaited(SettingsService.getInstance().then((s) => s.write(SettingsService.volume, newVolume)));
_showControlsFromPointerActivity(); _showControlsFromPointerActivity();
} }
} }
@@ -990,7 +990,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Save to settings // Save to settings
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
await settingsService.setRotationLocked(_isRotationLocked); await settingsService.write(SettingsService.rotationLocked, _isRotationLocked);
if (_isRotationLocked) { if (_isRotationLocked) {
// Locked: Allow landscape orientations only // Locked: Allow landscape orientations only
@@ -1085,8 +1085,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
appLogger.d('_loadPlaybackExtras: got client with serverId=${client.serverId}'); appLogger.d('_loadPlaybackExtras: got client with serverId=${client.serverId}');
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final introPattern = settings.getIntroPattern(); final introPattern = settings.read(SettingsService.introPattern);
final creditsPattern = settings.getCreditsPattern(); final creditsPattern = settings.read(SettingsService.creditsPattern);
final extras = await client.getPlaybackExtras( final extras = await client.getPlaybackExtras(
widget.metadata.ratingKey, widget.metadata.ratingKey,
introPattern: introPattern, introPattern: introPattern,
@@ -1172,8 +1172,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
return PlaybackExtras.withChapterFallback( return PlaybackExtras.withChapterFallback(
chapters: chapters, chapters: chapters,
markers: markers, markers: markers,
introPatternStr: settings.getIntroPattern(), introPatternStr: settings.read(SettingsService.introPattern),
creditsPatternStr: settings.getCreditsPattern(), creditsPatternStr: settings.read(SettingsService.creditsPattern),
); );
} }
@@ -2592,7 +2592,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
if (isVersionChange) { if (isVersionChange) {
final settingsService = await SettingsService.getInstance(); final settingsService = await SettingsService.getInstance();
final seriesKey = widget.metadata.grandparentRatingKey ?? widget.metadata.ratingKey; 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 // Preserve session identifiers across the reload so Plex reuses the
@@ -67,7 +67,7 @@ class _VolumeControlState extends State<VolumeControl> {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (mounted) { if (mounted) {
setState(() { setState(() {
_maxVolume = settings.getMaxVolume(); _maxVolume = settings.read(SettingsService.maxVolume);
}); });
} }
} }
@@ -89,7 +89,7 @@ class _VolumeControlState extends State<VolumeControl> {
final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble()); final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble());
widget.player.setVolume(newVolume); widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setVolume(newVolume); await settings.write(SettingsService.volume, newVolume);
} }
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
@@ -170,7 +170,7 @@ class _VolumeControlState extends State<VolumeControl> {
final newVolume = isMuted ? 100.0 : 0.0; final newVolume = isMuted ? 100.0 : 0.0;
widget.player.setVolume(newVolume); widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setVolume(newVolume); await settings.write(SettingsService.volume, newVolume);
}, },
), ),
); );
@@ -261,7 +261,7 @@ class _VolumeControlState extends State<VolumeControl> {
}, },
onChangeEnd: (value) async { onChangeEnd: (value) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.setVolume(value); await settings.write(SettingsService.volume, value);
}, },
activeColor: Colors.white, activeColor: Colors.white,
inactiveColor: Colors.white.withValues(alpha: 0.3), inactiveColor: Colors.white.withValues(alpha: 0.3),
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies:
sdk: flutter sdk: flutter
intl: ^0.20.2 intl: ^0.20.2
json_annotation: ^4.9.0 json_annotation: ^4.9.0
shared_preferences: ^2.2.2 shared_preferences: ^2.5.4
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
flutter_cache_manager: ^3.4.1 flutter_cache_manager: ^3.4.1
http: ^1.2.0 http: ^1.2.0