Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
2.4 MiB
Plaintext

{"repo": "axel10/vynody", "files": {"lib/player/settings/settings_service.dart": {"a88e60f807a364df4125ea6f017b1255da8ab6b6": {"t": 1785745913.2528586, "n": 78556, "h": "bdd404b5d59996e9", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum CloseWindowAction { ask, minimize, exit }\n\nextension CloseWindowActionX on CloseWindowAction {\n String get storageValue => name;\n static CloseWindowAction fromStorageValue(String? value) {\n if (value == 'minimize') return CloseWindowAction.minimize;\n if (value == 'exit') return CloseWindowAction.exit;\n return CloseWindowAction.ask;\n }\n}\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyEnableFadeEffect = 'enable_fade_effect';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyCloseToTray = 'close_to_tray';\n static const String _keyCloseWindowAction = 'close_window_action';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyEqualizerBandCount = 'equalizer_band_count';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeToTrayProperty = SettingProperty<bool>(\n key: _keyCloseToTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeWindowActionProperty = SettingProperty<CloseWindowAction>(\n key: _keyCloseWindowAction,\n defaultValue: CloseWindowAction.ask,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final val = prefs.getString(key);\n if (val != null) {\n return CloseWindowActionX.fromStorageValue(val);\n }\n if (prefs.containsKey(_keyCloseToTray)) {\n final legacyCloseToTray = prefs.getBool(_keyCloseToTray);\n if (legacyCloseToTray == false) {\n return CloseWindowAction.exit;\n }\n }\n return def;\n },\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableFadeEffectProperty = SettingProperty<bool>(\n key: _keyEnableFadeEffect,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _equalizerBandCountProperty = SettingProperty<int>(\n key: _keyEqualizerBandCount,\n defaultValue: 10,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n CloseWindowAction get closeWindowAction => _closeWindowActionProperty.value;\n set closeWindowAction(CloseWindowAction value) {\n _closeWindowActionProperty.value = value;\n _closeToTrayProperty.value = (value == CloseWindowAction.minimize);\n }\n\n bool get closeToTray => closeWindowAction == CloseWindowAction.minimize;\n set closeToTray(bool value) {\n closeWindowAction = value ? CloseWindowAction.minimize : CloseWindowAction.exit;\n }\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get enableFadeEffect => _enableFadeEffectProperty.value;\n set enableFadeEffect(bool value) =>\n _enableFadeEffectProperty.value = value;\n\n int get equalizerBandCount => _equalizerBandCountProperty.value;\n set equalizerBandCount(int value) =>\n _equalizerBandCountProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "94d9e5b54e782ca63f4e96b3179ab11cded04f12": {"t": 1785757859.355269, "n": 78556, "h": "bdd404b5d59996e9", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum CloseWindowAction { ask, minimize, exit }\n\nextension CloseWindowActionX on CloseWindowAction {\n String get storageValue => name;\n static CloseWindowAction fromStorageValue(String? value) {\n if (value == 'minimize') return CloseWindowAction.minimize;\n if (value == 'exit') return CloseWindowAction.exit;\n return CloseWindowAction.ask;\n }\n}\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyEnableFadeEffect = 'enable_fade_effect';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyCloseToTray = 'close_to_tray';\n static const String _keyCloseWindowAction = 'close_window_action';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyEqualizerBandCount = 'equalizer_band_count';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeToTrayProperty = SettingProperty<bool>(\n key: _keyCloseToTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeWindowActionProperty = SettingProperty<CloseWindowAction>(\n key: _keyCloseWindowAction,\n defaultValue: CloseWindowAction.ask,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final val = prefs.getString(key);\n if (val != null) {\n return CloseWindowActionX.fromStorageValue(val);\n }\n if (prefs.containsKey(_keyCloseToTray)) {\n final legacyCloseToTray = prefs.getBool(_keyCloseToTray);\n if (legacyCloseToTray == false) {\n return CloseWindowAction.exit;\n }\n }\n return def;\n },\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableFadeEffectProperty = SettingProperty<bool>(\n key: _keyEnableFadeEffect,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _equalizerBandCountProperty = SettingProperty<int>(\n key: _keyEqualizerBandCount,\n defaultValue: 10,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n CloseWindowAction get closeWindowAction => _closeWindowActionProperty.value;\n set closeWindowAction(CloseWindowAction value) {\n _closeWindowActionProperty.value = value;\n _closeToTrayProperty.value = (value == CloseWindowAction.minimize);\n }\n\n bool get closeToTray => closeWindowAction == CloseWindowAction.minimize;\n set closeToTray(bool value) {\n closeWindowAction = value ? CloseWindowAction.minimize : CloseWindowAction.exit;\n }\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get enableFadeEffect => _enableFadeEffectProperty.value;\n set enableFadeEffect(bool value) =>\n _enableFadeEffectProperty.value = value;\n\n int get equalizerBandCount => _equalizerBandCountProperty.value;\n set equalizerBandCount(int value) =>\n _equalizerBandCountProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "7dc989227df959c87fac216a6347ea19073153f2": {"t": 1785757861.410264, "n": 78163, "h": "4aced4e33d9db605", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum CloseWindowAction { ask, minimize, exit }\n\nextension CloseWindowActionX on CloseWindowAction {\n String get storageValue => name;\n static CloseWindowAction fromStorageValue(String? value) {\n if (value == 'minimize') return CloseWindowAction.minimize;\n if (value == 'exit') return CloseWindowAction.exit;\n return CloseWindowAction.ask;\n }\n}\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyCloseToTray = 'close_to_tray';\n static const String _keyCloseWindowAction = 'close_window_action';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyEqualizerBandCount = 'equalizer_band_count';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeToTrayProperty = SettingProperty<bool>(\n key: _keyCloseToTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeWindowActionProperty = SettingProperty<CloseWindowAction>(\n key: _keyCloseWindowAction,\n defaultValue: CloseWindowAction.ask,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final val = prefs.getString(key);\n if (val != null) {\n return CloseWindowActionX.fromStorageValue(val);\n }\n if (prefs.containsKey(_keyCloseToTray)) {\n final legacyCloseToTray = prefs.getBool(_keyCloseToTray);\n if (legacyCloseToTray == false) {\n return CloseWindowAction.exit;\n }\n }\n return def;\n },\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _equalizerBandCountProperty = SettingProperty<int>(\n key: _keyEqualizerBandCount,\n defaultValue: 10,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n CloseWindowAction get closeWindowAction => _closeWindowActionProperty.value;\n set closeWindowAction(CloseWindowAction value) {\n _closeWindowActionProperty.value = value;\n _closeToTrayProperty.value = (value == CloseWindowAction.minimize);\n }\n\n bool get closeToTray => closeWindowAction == CloseWindowAction.minimize;\n set closeToTray(bool value) {\n closeWindowAction = value ? CloseWindowAction.minimize : CloseWindowAction.exit;\n }\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n int get equalizerBandCount => _equalizerBandCountProperty.value;\n set equalizerBandCount(int value) =>\n _equalizerBandCountProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "309f9cd0bde199ed4125addd5765d44f00f6abe4": {"t": 1785757863.0007532, "n": 77759, "h": "baeba421dfa19f3e", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum CloseWindowAction { ask, minimize, exit }\n\nextension CloseWindowActionX on CloseWindowAction {\n String get storageValue => name;\n static CloseWindowAction fromStorageValue(String? value) {\n if (value == 'minimize') return CloseWindowAction.minimize;\n if (value == 'exit') return CloseWindowAction.exit;\n return CloseWindowAction.ask;\n }\n}\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyCloseToTray = 'close_to_tray';\n static const String _keyCloseWindowAction = 'close_window_action';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeToTrayProperty = SettingProperty<bool>(\n key: _keyCloseToTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeWindowActionProperty = SettingProperty<CloseWindowAction>(\n key: _keyCloseWindowAction,\n defaultValue: CloseWindowAction.ask,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final val = prefs.getString(key);\n if (val != null) {\n return CloseWindowActionX.fromStorageValue(val);\n }\n if (prefs.containsKey(_keyCloseToTray)) {\n final legacyCloseToTray = prefs.getBool(_keyCloseToTray);\n if (legacyCloseToTray == false) {\n return CloseWindowAction.exit;\n }\n }\n return def;\n },\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n CloseWindowAction get closeWindowAction => _closeWindowActionProperty.value;\n set closeWindowAction(CloseWindowAction value) {\n _closeWindowActionProperty.value = value;\n _closeToTrayProperty.value = (value == CloseWindowAction.minimize);\n }\n\n bool get closeToTray => closeWindowAction == CloseWindowAction.minimize;\n set closeToTray(bool value) {\n closeWindowAction = value ? CloseWindowAction.minimize : CloseWindowAction.exit;\n }\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "9db9c37e5cd1890e2cb049e640fca3e51d08e4f2": {"t": 1785757864.6355865, "n": 76315, "h": "ecbcd5bca29d71bd", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyCloseToTray = 'close_to_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeToTrayProperty = SettingProperty<bool>(\n key: _keyCloseToTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n bool get closeToTray => _closeToTrayProperty.value;\n set closeToTray(bool value) => _closeToTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "e0f717cc5f22052a820fcc552a710ff5c990df87": {"t": 1785757866.4176772, "n": 75968, "h": "f704250e082d7a66", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "9adcf656d7407de94a5a8efd02efcd5bf8ceedc2": {"t": 1785757868.013437, "n": 75968, "h": "c516cbc4e76362a7", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 2.0;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "5e502d948290f7b91fb6705077b1284208b92fdd": {"t": 1785757869.7721481, "n": 75269, "h": "3a943b4ec5eb283b", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "c3b582e114622a2dd8803c0eadc999f8425615bd": {"t": 1785757871.9248912, "n": 75340, "h": "dd71e8f703b39d8c", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "01a7041538b13b496f671b392c3d86ec08e7dd71": {"t": 1785757873.5155332, "n": 75156, "h": "f4bbfac0f6f7dea2", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "08b3b57fa7b05679b4e2702e679c7338c42def46": {"t": 1785757875.212862, "n": 74636, "h": "32898c6325578c5c", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "beed06f6c0fd71fc946fb60cb063668e2ed5431e": {"t": 1785757877.2040231, "n": 74200, "h": "3f1a1aad3cefe1fc", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "9c3ab19864b2c4f9424db2fcf2e58b05f43e4302": {"t": 1785757879.2239404, "n": 73665, "h": "579ce1e2dfbcdb00", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "3567d8b8a59cc5ef77fcfacded90612be80b4df7": {"t": 1785757880.7994192, "n": 72804, "h": "ebef30a28f692a43", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "09be4a89868736117226376037eabb561f2db707": {"t": 1785757882.7290423, "n": 72804, "h": "7e1054a6e897f4ec", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "1be04dc42ae4a300343036f56b1f645d6f4a60ca": {"t": 1785757884.5275207, "n": 72805, "h": "cabde5caeb551689", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "64e3fbca6c628a4359a993cea4ee0759a70c574f": {"t": 1785757886.2790983, "n": 72310, "h": "f4200510a829f49a", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "97ebb6e2a81f7d6a1dde55f953719d2fdd90d0cf": {"t": 1785757887.9919486, "n": 71877, "h": "3c51493b409504a1", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "9b7e3a20e8bf1a64bf1fac474826685daa914a23": {"t": 1785757891.2349608, "n": 70973, "h": "b5ffe8e9300d7e01", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "81c131f6b2a7169c7fa422ab1e4e50c04aff9759": {"t": 1785757893.8329904, "n": 70975, "h": "21be30d9540e8965", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "8f5a811372867d45ba92423f17ad27869c980c82": {"t": 1785757895.4419305, "n": 70859, "h": "5edeb1c20f162a66", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "4ccd848ed27d61dc564c0cf509fc6caa023f3172": {"t": 1785757897.2961488, "n": 70861, "h": "9fe24547a8a00d35", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "0c657c9e98e846e179a0bc7c279f443e4472d8b8": {"t": 1785757898.757059, "n": 70400, "h": "5babb5c5f6f13934", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "30d3f2b0e038755b68206219f560fa09db5490dd": {"t": 1785757900.4062343, "n": 69966, "h": "5930356d71b57a2a", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "5c2b586b8b6dd5ed9fdbb215b9cae39e25e569ca": {"t": 1785757902.0630658, "n": 69978, "h": "8b465bfc040f0c65", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.traditional.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.traditional,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "ec9ea77e30d4bc9f73e7a49f689795486cd97bd1": {"t": 1785757904.0389152, "n": 68490, "h": "6b4ca5491faa384c", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.traditional.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleProperty = SettingProperty<double>(\n key: _keyLyricsFontScale,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _normalizeLyricsFontScale(prefs.getDouble(key) ?? def),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.traditional,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => _lyricsFontScaleProperty.value;\n set lyricsFontScale(double value) => _lyricsFontScaleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n _lyricsFontScaleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "daabb56f1e19d0659b293d32ff201dd9c4849088": {"t": 1785757905.6488197, "n": 68283, "h": "cfc0e412b55c1409", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.traditional.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleProperty = SettingProperty<double>(\n key: _keyLyricsFontScale,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _normalizeLyricsFontScale(prefs.getDouble(key) ?? def),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: 0.20,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: 0.40,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.traditional,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => _lyricsFontScaleProperty.value;\n set lyricsFontScale(double value) => _lyricsFontScaleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n _lyricsFontScaleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "9ff6963c878d9aaf843cfb968197893044d757f3": {"t": 1785757907.3077927, "n": 67715, "h": "2938ddb0ed91d7e3", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.hybrid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleProperty = SettingProperty<double>(\n key: _keyLyricsFontScale,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _normalizeLyricsFontScale(prefs.getDouble(key) ?? def),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: 0.20,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: 0.40,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n double get lyricsFontScale => _lyricsFontScaleProperty.value;\n set lyricsFontScale(double value) => _lyricsFontScaleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n _lyricsFontScaleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "fc3ceb7ded0ac279796ee336ddc15abb76fe6575": {"t": 1785757909.2414174, "n": 67603, "h": "1472a74f1b74f0fb", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.grid => 'grid',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleProperty = SettingProperty<double>(\n key: _keyLyricsFontScale,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _normalizeLyricsFontScale(prefs.getDouble(key) ?? def),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: 0.20,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: 0.40,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n double get lyricsFontScale => _lyricsFontScaleProperty.value;\n set lyricsFontScale(double value) => _lyricsFontScaleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n _lyricsFontScaleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "cedfc8a8d5727b44054ac5eb93b7fd045ac7e33d": {"t": 1785757911.2053192, "n": 67210, "h": "2922b5f47450eab8", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.grid => 'grid',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleProperty = SettingProperty<double>(\n key: _keyLyricsFontScale,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _normalizeLyricsFontScale(prefs.getDouble(key) ?? def),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: 0.20,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: 0.40,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n double get lyricsFontScale => _lyricsFontScaleProperty.value;\n set lyricsFontScale(double value) => _lyricsFontScaleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n _lyricsFontScaleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "539bdf4adb5ca9158b37c6af952a624d145f8c52": {"t": 1785757913.1911712, "n": 66744, "h": "474fc29d51b2e395", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.grid => 'grid',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.8;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleProperty = SettingProperty<double>(\n key: _keyLyricsFontScale,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _normalizeLyricsFontScale(prefs.getDouble(key) ?? def),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: Platform.isWindows || Platform.isLinux || Platform.isMacOS,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: 0.20,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: 0.40,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: 30.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentSystemLanguageCode();\n }\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n double get lyricsFontScale => _lyricsFontScaleProperty.value;\n set lyricsFontScale(double value) => _lyricsFontScaleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n _playbackBlurredArtworkBlurSigmaProperty.value;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n _playbackCustomImageBlurSigmaProperty.value;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n _lyricsFontScaleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size? savedRegularWindowSize;\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "a9178df4584ef6af7cc203f85c6c763bd3d89ef4": {"t": 1786067503.9774637, "n": 75269, "h": "3a943b4ec5eb283b", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}, "dd1bbd43ab9065a93ea80d20336e16c1c8a199f9": {"t": 1786068342.8974783, "n": 82163, "h": "23d8e25d34861f7c", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:audio_core/audio_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'package:vynody/player/settings/shortcut_bindings.dart';\nimport 'package:vynody/transcode/transcode_models.dart';\nimport 'package:vynody/utils/language_code_utils.dart';\n\nimport 'package:vynody/utils/localized_text.dart';\n\nAppLocalizations _l10n() => currentAppL10n;\n\nenum LyricsAiProvider { googleAiStudio, openRouter, doubao, deepseek, custom }\n\nenum LyricsAiModelPurpose { generation, translation }\n\nenum LyricsAiModelSlot { primary, fallback }\n\nenum LyricsSaveMethod { original, embedded, lrcFile }\n\nenum LyricsStyle { traditional, apple }\n\nfinal class LyricsAiModelSelection {\n const LyricsAiModelSelection({required this.provider, required this.modelId});\n\n final LyricsAiProvider provider;\n final String modelId;\n\n bool get isEmpty => modelId.trim().isEmpty;\n\n LyricsAiModelSelection copyWith({\n LyricsAiProvider? provider,\n String? modelId,\n }) {\n return LyricsAiModelSelection(\n provider: provider ?? this.provider,\n modelId: modelId ?? this.modelId,\n );\n }\n\n @override\n bool operator ==(Object other) {\n return identical(this, other) ||\n other is LyricsAiModelSelection &&\n other.provider == provider &&\n other.modelId == modelId;\n }\n\n @override\n int get hashCode => Object.hash(provider, modelId);\n}\n\nenum SmallWindowBottomPanelMode { collapsed, queue, lyrics }\n\nenum CloseWindowAction { ask, minimize, exit }\n\nextension CloseWindowActionX on CloseWindowAction {\n String get storageValue => name;\n static CloseWindowAction fromStorageValue(String? value) {\n if (value == 'minimize') return CloseWindowAction.minimize;\n if (value == 'exit') return CloseWindowAction.exit;\n return CloseWindowAction.ask;\n }\n}\n\nenum FolderViewMode { list, hybrid, grid }\n\nextension FolderViewModeX on FolderViewMode {\n String get storageValue => switch (this) {\n FolderViewMode.list => 'list',\n FolderViewMode.hybrid => 'grid',\n FolderViewMode.grid => 'grid_all',\n };\n\n static FolderViewMode fromStorageValue(String? value, FolderViewMode defaultValue) {\n switch (value?.trim().toLowerCase()) {\n case 'grid':\n return FolderViewMode.hybrid;\n case 'grid_all':\n return FolderViewMode.grid;\n case 'list':\n return FolderViewMode.list;\n default:\n return defaultValue;\n }\n }\n}\n\nextension ThemeModeX on ThemeMode {\n String get storageValue => switch (this) {\n ThemeMode.system => 'system',\n ThemeMode.light => 'light',\n ThemeMode.dark => 'dark',\n };\n\n static ThemeMode fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'light':\n return ThemeMode.light;\n case 'dark':\n return ThemeMode.dark;\n case 'system':\n default:\n return ThemeMode.system;\n }\n }\n}\n\nextension LyricsAiProviderX on LyricsAiProvider {\n String get storageValue => switch (this) {\n LyricsAiProvider.googleAiStudio => 'google_ai_studio',\n LyricsAiProvider.openRouter => 'openrouter',\n LyricsAiProvider.doubao => 'doubao',\n LyricsAiProvider.deepseek => 'deepseek',\n LyricsAiProvider.custom => 'custom',\n };\n\n String get displayName {\n if (this == LyricsAiProvider.custom) {\n return SettingsService._lastKnownCustomProviderName.trim().isEmpty\n ? _l10n().custom\n : SettingsService._lastKnownCustomProviderName.trim();\n }\n return switch (this) {\n LyricsAiProvider.googleAiStudio =>\n 'Google AI Studio',\n LyricsAiProvider.openRouter => 'OpenRouter',\n LyricsAiProvider.doubao => _l10n().doubao,\n LyricsAiProvider.deepseek => 'DeepSeek',\n LyricsAiProvider.custom => '',\n };\n }\n\n static LyricsAiProvider fromStorageValue(String? value) {\n switch (value?.trim().toLowerCase()) {\n case 'openrouter':\n return LyricsAiProvider.openRouter;\n case 'doubao':\n return LyricsAiProvider.doubao;\n case 'deepseek':\n return LyricsAiProvider.deepseek;\n case 'custom':\n return LyricsAiProvider.custom;\n case 'google_ai_studio':\n case 'google':\n case 'gemini':\n default:\n return LyricsAiProvider.googleAiStudio;\n }\n }\n}\n\nclass SettingProperty<T> {\n final String key;\n final T defaultValue;\n final SharedPreferences prefs;\n final VoidCallback? onChanged;\n final T Function(SharedPreferences prefs, String key, T defaultValue)?\n customRead;\n final void Function(SharedPreferences prefs, String key, T value)?\n customWrite;\n\n T _value;\n\n SettingProperty({\n required this.key,\n required this.defaultValue,\n required this.prefs,\n this.onChanged,\n this.customRead,\n this.customWrite,\n }) : _value = _read(prefs, key, defaultValue, customRead);\n\n T get value => _value;\n\n set value(T newValue) {\n if (_value == newValue) return;\n _value = newValue;\n _write(prefs, key, newValue, customWrite);\n onChanged?.call();\n }\n\n static T _read<T>(\n SharedPreferences prefs,\n String key,\n T defaultValue,\n T Function(SharedPreferences prefs, String key, T defaultValue)? customRead,\n ) {\n if (customRead != null) {\n return customRead(prefs, key, defaultValue);\n }\n if (T == bool) {\n return (prefs.getBool(key) ?? defaultValue) as T;\n }\n if (T == int) {\n return (prefs.getInt(key) ?? defaultValue) as T;\n }\n if (T == double) {\n return (prefs.getDouble(key) ?? defaultValue) as T;\n }\n if (T == String) {\n return (prefs.getString(key) ?? defaultValue) as T;\n }\n return defaultValue;\n }\n\n static void _write<T>(\n SharedPreferences prefs,\n String key,\n T value,\n void Function(SharedPreferences prefs, String key, T value)? customWrite,\n ) {\n if (customWrite != null) {\n customWrite(prefs, key, value);\n return;\n }\n if (value is bool) {\n prefs.setBool(key, value);\n } else if (value is int) {\n prefs.setInt(key, value);\n } else if (value is double) {\n prefs.setDouble(key, value);\n } else if (value is String) {\n prefs.setString(key, value);\n }\n }\n\n void reset() {\n value = defaultValue;\n }\n}\n\nclass SettingsService extends ChangeNotifier {\n static const String defaultGenerationPrimaryModelId =\n 'gemini-flash-lite-latest';\n static const String defaultGenerationFallbackModelId = '';\n static const String defaultTranslationPrimaryModelId = 'gemma-4-31b-it';\n static const String defaultTranslationFallbackModelId = '';\n static const String defaultOpenRouterGenerationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultOpenRouterTranslationModelId =\n 'google/gemini-3.1-flash-lite';\n static const String defaultDoubaoGenerationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDoubaoTranslationModelId =\n 'doubao-seed-2-0-lite-260428';\n static const String defaultDeepSeekTranslationModelId = 'deepseek-v4-flash';\n static const String _keyThemeMode = 'theme_mode';\n static const String _keyLocale = 'app_locale';\n static const String _keyEnableFadeEffect = 'enable_fade_effect';\n static const String _keyWindowsAutoRepairShortcut = 'windows_auto_repair_shortcut';\n static const String _keyEnableSystemTray = 'enable_system_tray';\n static const String _keyCloseToTray = 'close_to_tray';\n static const String _keyCloseWindowAction = 'close_window_action';\n static const String _keyImmersiveTabBar = 'immersive_tab_bar_enabled';\n static const List<String> defaultTopButtonsOrder = [\n 'more',\n 'favorite',\n 'playlist_mode',\n 'shuffle',\n 'tag_completion',\n 'sleep_timer',\n 'equalizer',\n ];\n static const String defaultMainControlsLeftButton = 'visualizer';\n static const String defaultMainControlsRightButton = 'volume';\n static const String defaultLyricsHeaderRightButton = 'favorite';\n\n static const String _keyTopButtonsOrder = 'top_buttons_order';\n static const String _keyMainControlsLeftButton = 'main_controls_left_button';\n static const String _keyMainControlsRightButton = 'main_controls_right_button';\n static const String _keyLyricsHeaderRightButton = 'lyrics_header_right_button';\n\n static const String _keyCollapseButtonsInLandscapeLyrics =\n 'collapse_buttons_in_landscape_lyrics';\n static const String _keyShowScanProgressToast = 'show_scan_progress_toast';\n static const String _keyOpenPlaybackOnDirectorySongTap =\n 'open_playback_on_directory_song_tap';\n static const String _keyDefaultToLyricsModeOnPlaybackOpen =\n 'default_to_lyrics_mode_on_playback_open';\n static const String _keySampleStride = 'sample_stride';\n static const String _keyWaveformChunks = 'waveform_chunks';\n static const String geminiApiKeyStorageKey = 'gemini_api_key';\n static const String openRouterApiKeyStorageKey = 'openrouter_api_key';\n static const String doubaoApiKeyStorageKey = 'doubao_api_key';\n static const String deepseekApiKeyStorageKey = 'deepseek_api_key';\n static const String customProviderApiKeyStorageKey = 'custom_provider_api_key';\n static const String customProviderBaseUrlStorageKey = 'custom_provider_base_url';\n static const String customProviderNameStorageKey = 'custom_provider_name';\n static String _lastKnownCustomProviderName = '';\n static const String _keyLyricsTranslationTargetLanguage =\n 'lyrics_translation_target_language';\n static const String _keyLyricsSaveMethod = 'lyrics_save_method';\n static const String _keyLyricsStyle = 'lyrics_style';\n static const String _keyLyricsFontScale = 'lyrics_font_scale';\n static const String _keyLyricsFontScaleTraditional = 'lyrics_font_scale_traditional';\n static const String _keyLyricsFontScaleApple = 'lyrics_font_scale_apple';\n static const String _keyGenerationPrimaryProvider =\n 'lyrics_generation_primary_provider';\n static const String _keyGenerationPrimaryModelId =\n 'lyrics_generation_primary_model_id';\n static const String _keyGenerationFallbackProvider =\n 'lyrics_generation_fallback_provider';\n static const String _keyGenerationFallbackModelId =\n 'lyrics_generation_fallback_model_id';\n static const String _keyTranslationPrimaryProvider =\n 'lyrics_translation_primary_provider';\n static const String _keyTranslationPrimaryModelId =\n 'lyrics_translation_primary_model_id';\n static const String _keyTranslationFallbackProvider =\n 'lyrics_translation_fallback_provider';\n static const String _keyTranslationFallbackModelId =\n 'lyrics_translation_fallback_model_id';\n static const String _legacyKeyLyricsAiProvider = 'lyrics_ai_provider';\n static const String _legacyKeyGeminiPrimaryModelId =\n 'gemini_primary_model_id';\n static const String _legacyKeyGeminiFallbackModelId =\n 'gemini_fallback_model_id';\n static const String _legacyKeyGeminiTranslationModelId =\n 'gemini_translation_model_id';\n static const String acoustidApiKeyStorageKey = 'acoustid_api_key';\n static const String _keyShortcutBindings = 'shortcut_bindings';\n static const String _builtInAcoustidApiKey = 'raGXgwxqws';\n static const int defaultSampleStride = 1;\n static const double defaultLyricsFontScale = 1.0;\n static const double minLyricsFontScale = 0.5;\n static const double maxLyricsFontScale = 1.5;\n static const double lyricsFontScaleStep = 0.1;\n\n static const double defaultPlaybackBackgroundNormalOpacity = 0.20;\n static const double defaultPlaybackBackgroundLyricsOpacity = 0.30;\n static const double defaultPlaybackBlurredArtworkBlurSigma = 30.0;\n static const double defaultPlaybackCustomImageBlurSigma = 0.0;\n\n // Visualizer styling keys\n static const String _keyVisColor = 'visualizer_color';\n static const String _keyVisOpacity = 'visualizer_opacity';\n static const String _keyVisGradient = 'visualizer_gradient_enabled';\n static const String _keyVisStartColor = 'visualizer_start_color';\n static const String _keyVisEndColor = 'visualizer_end_color';\n static const String _keyVisGradientStop1 = 'visualizer_gradient_stop_1';\n static const String _keyVisGradientStop2 = 'visualizer_gradient_stop_2';\n static const String _keyVisGradientTileMode = 'visualizer_gradient_tile_mode';\n static const String _keyVisualizerDynamicColor = 'visualizer_dynamic_color';\n static const String _keyVisualizerDynamicStartColor =\n 'visualizer_dynamic_start_color';\n static const String _keyVisualizerDynamicEndColor =\n 'visualizer_dynamic_end_color';\n static const String _keyPlaybackBackgroundType = 'playback_background_type';\n static const String _keyPlaybackRadialGradientEnabled =\n 'playback_radial_gradient_enabled';\n static const String _keyPlaybackBackgroundColor = 'playback_background_color';\n static const String _keyPlaybackBackgroundCustomImagePath =\n 'playback_background_custom_image_path';\n static const String _keyPlaybackBackgroundNormalOpacity =\n 'playback_background_normal_opacity';\n static const String _keyPlaybackBackgroundLyricsOpacity =\n 'playback_background_lyrics_opacity';\n static const String _keyPlaybackBlurredArtworkBlurSigma =\n 'playback_blurred_artwork_blur_sigma';\n static const String _keyPlaybackCustomImageBlurSigma =\n 'playback_custom_image_blur_sigma';\n static const String _keyPlaybackMeshBackgroundSpeed =\n 'playback_mesh_background_speed';\n static const String _keyIsAutoMode = 'visualizer_auto_mode';\n static const String _keyAutoSpectrumQuantity =\n 'visualizer_auto_spectrum_quantity';\n static const String _keyAutoSpeed = 'visualizer_auto_speed';\n static const String _keyPortraitFrequencyGroups =\n 'visualizer_portrait_frequency_groups';\n static const String _keyLandscapeFrequencyGroups =\n 'visualizer_landscape_frequency_groups';\n static const String _keyPortraitGap = 'visualizer_portrait_gap';\n static const String _keyLandscapeGap = 'visualizer_landscape_gap';\n static const String _keyIsWaveformProgressBarEnabled =\n 'waveform_progress_bar_enabled';\n static const String _keyWaveformLongPressSeekSpeed =\n 'waveform_long_press_seek_speed';\n static const String _keyEnableWaveformLongPressSeek =\n 'enable_waveform_long_press_seek';\n static const double defaultWaveformLongPressSeekSpeed = 2.0;\n static const double minWaveformLongPressSeekSpeed = 1.1;\n static const double maxWaveformLongPressSeekSpeed = 5.0;\n static const String _keyPlaybackSpeedLimit5x = 'playback_speed_limit_5x';\n static const String _keyEqualizerBandCount = 'equalizer_band_count';\n static const String _keyShowDeveloperOptions = 'show_developer_options';\n static const String skipShortAudioScanEnabledStorageKey =\n 'scan_skip_short_audio_enabled';\n static const String skipShortAudioScanMinimumDurationSecondsStorageKey =\n 'scan_skip_short_audio_min_duration_seconds';\n static const int defaultSkipShortAudioScanMinimumDurationSeconds = 30;\n static const String _keyRandomRange = 'random_range';\n static const String _keyRandomMethod = 'random_method';\n static const String _keyTranscodeDefaultFormat =\n 'transcode_default_output_format';\n static const String _keyTranscodeDefaultQualityTier =\n 'transcode_default_quality_tier';\n\n static const String _keyRegularWindowWidth = 'regular_window_width';\n static const String _keyRegularWindowHeight = 'regular_window_height';\n static const String _keySmallWindowWidth = 'small_window_width';\n static const String _keySmallWindowHeight = 'small_window_height';\n static const String _keySmallWindowBottomPanelMode =\n 'small_window_bottom_panel_mode';\n static const String _keySmallWindowAlwaysOnTop = 'small_window_always_on_top';\n static const String _keySmallWindowQueueWidth = 'small_window_queue_width';\n static const String _keySmallWindowQueueHeight = 'small_window_queue_height';\n static const String _keyHasShownOnboarding = 'has_shown_onboarding';\n static const String _keyHasShownCoverTapLyricTip = 'has_shown_cover_tap_lyric_tip';\n static const String _keyHasShownLyricsMenuTip = 'has_shown_lyrics_menu_tip';\n static const String _keyTagCompletionSaveToSourceFile =\n 'tag_completion_save_to_source_file';\n static const String _keyLanSharingEnabled = 'lan_sharing_enabled';\n static const String _keyLanSharingFolderPath = 'lan_sharing_folder_path';\n static const String _keyFolderViewMode = 'folder_view_mode';\n static const String _keyUiScale = 'ui_scale';\n static const double defaultUiScale = 1.0;\n static const double minUiScale = 0.8;\n static const double maxUiScale = 1.5;\n\n final SharedPreferences _prefs;\n bool _isUserInactive = false;\n Timer? _inactivityTimer;\n Map<String, ShortcutBinding> _shortcutBindings;\n\n // SettingProperty definitions for all configuration options\n late final _regularWindowWidthProperty = SettingProperty<double>(\n key: _keyRegularWindowWidth,\n defaultValue: 1280.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _regularWindowHeightProperty = SettingProperty<double>(\n key: _keyRegularWindowHeight,\n defaultValue: 720.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownOnboardingProperty = SettingProperty<bool>(\n key: _keyHasShownOnboarding,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownCoverTapLyricTipProperty = SettingProperty<bool>(\n key: _keyHasShownCoverTapLyricTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _hasShownLyricsMenuTipProperty = SettingProperty<bool>(\n key: _keyHasShownLyricsMenuTip,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _tagCompletionSaveToSourceFileProperty = SettingProperty<bool>(\n key: _keyTagCompletionSaveToSourceFile,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingEnabledProperty = SettingProperty<bool>(\n key: _keyLanSharingEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lanSharingFolderPathProperty = SettingProperty<String>(\n key: _keyLanSharingFolderPath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _folderViewModeProperty = SettingProperty<FolderViewMode>(\n key: _keyFolderViewMode,\n defaultValue: FolderViewMode.grid,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n FolderViewModeX.fromStorageValue(prefs.getString(key), def),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _uiScaleProperty = SettingProperty<double>(\n key: _keyUiScale,\n defaultValue: defaultUiScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n (prefs.getDouble(key) ?? def).clamp(minUiScale, maxUiScale),\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, val.clamp(minUiScale, maxUiScale)),\n );\n\n late final _themeModeProperty = SettingProperty<ThemeMode>(\n key: _keyThemeMode,\n defaultValue: ThemeMode.system,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n ThemeModeX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final SettingProperty<String> _localeProperty = SettingProperty<String>(\n key: _keyLocale,\n defaultValue: 'system',\n prefs: _prefs,\n onChanged: () {\n LocalizedText.overrideLanguageCode = _localeProperty.value;\n notifyListeners();\n },\n );\n\n late final _isImmersiveTabBarEnabledProperty = SettingProperty<bool>(\n key: _keyImmersiveTabBar,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _collapseButtonsInLandscapeLyricsProperty = SettingProperty<bool>(\n key: _keyCollapseButtonsInLandscapeLyrics,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _topButtonsOrderProperty = SettingProperty<List<String>>(\n key: _keyTopButtonsOrder,\n defaultValue: defaultTopButtonsOrder,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final raw = prefs.getString(key);\n if (raw == null || raw.trim().isEmpty) return def;\n try {\n final List<dynamic> list = jsonDecode(raw);\n final result = list.map((e) => e.toString()).toList();\n if (result.length == 7) return result;\n } catch (e) {\n debugPrint('Failed to parse top_buttons_order: $e');\n }\n return def;\n },\n customWrite: (prefs, key, value) {\n prefs.setString(key, jsonEncode(value));\n },\n );\n\n late final _mainControlsLeftButtonProperty = SettingProperty<String>(\n key: _keyMainControlsLeftButton,\n defaultValue: defaultMainControlsLeftButton,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _mainControlsRightButtonProperty = SettingProperty<String>(\n key: _keyMainControlsRightButton,\n defaultValue: defaultMainControlsRightButton,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsHeaderRightButtonProperty = SettingProperty<String>(\n key: _keyLyricsHeaderRightButton,\n defaultValue: defaultLyricsHeaderRightButton,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showScanProgressToastProperty = SettingProperty<bool>(\n key: _keyShowScanProgressToast,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _openPlaybackOnDirectorySongTapProperty = SettingProperty<bool>(\n key: _keyOpenPlaybackOnDirectorySongTap,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _defaultToLyricsModeOnPlaybackOpenProperty = SettingProperty<bool>(\n key: _keyDefaultToLyricsModeOnPlaybackOpen,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _windowsAutoRepairShortcutProperty = SettingProperty<bool>(\n key: _keyWindowsAutoRepairShortcut,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableSystemTrayProperty = SettingProperty<bool>(\n key: _keyEnableSystemTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeToTrayProperty = SettingProperty<bool>(\n key: _keyCloseToTray,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _closeWindowActionProperty = SettingProperty<CloseWindowAction>(\n key: _keyCloseWindowAction,\n defaultValue: CloseWindowAction.ask,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final val = prefs.getString(key);\n if (val != null) {\n return CloseWindowActionX.fromStorageValue(val);\n }\n if (prefs.containsKey(_keyCloseToTray)) {\n final legacyCloseToTray = prefs.getBool(_keyCloseToTray);\n if (legacyCloseToTray == false) {\n return CloseWindowAction.exit;\n }\n }\n return def;\n },\n customWrite: (prefs, key, val) => prefs.setString(key, val.storageValue),\n );\n\n late final _waveformChunksProperty = SettingProperty<int>(\n key: _keyWaveformChunks,\n defaultValue: 120,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _sampleStrideProperty = SettingProperty<int>(\n key: _keySampleStride,\n defaultValue: defaultSampleStride,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsTranslationTargetLanguageProperty = SettingProperty<String>(\n key: _keyLyricsTranslationTargetLanguage,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n final value = LanguageCodeUtils.normalizeLanguageCode(\n prefs.getString(key),\n );\n return value.isEmpty ? def : value;\n },\n customWrite: (prefs, key, val) {\n final normalized = LanguageCodeUtils.normalizeLanguageCode(val);\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _lyricsSaveMethodProperty = SettingProperty<String>(\n key: _keyLyricsSaveMethod,\n defaultValue: LyricsSaveMethod.original.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsStyleProperty = SettingProperty<String>(\n key: _keyLyricsStyle,\n defaultValue: LyricsStyle.apple.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _lyricsFontScaleTraditionalProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleTraditional,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _lyricsFontScaleAppleProperty = SettingProperty<double>(\n key: _keyLyricsFontScaleApple,\n defaultValue: defaultLyricsFontScale,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) {\n if (!prefs.containsKey(key) && prefs.containsKey(_keyLyricsFontScale)) {\n return _normalizeLyricsFontScale(prefs.getDouble(_keyLyricsFontScale) ?? def);\n }\n return _normalizeLyricsFontScale(prefs.getDouble(key) ?? def);\n },\n customWrite: (prefs, key, val) =>\n prefs.setDouble(key, _normalizeLyricsFontScale(val)),\n );\n\n late final _generationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyGenerationPrimaryModelId,\n defaultValue: defaultGenerationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiPrimaryModelId,\n defaultValue: def,\n ),\n );\n\n late final _generationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyGenerationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _generationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyGenerationFallbackModelId,\n defaultValue: defaultGenerationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiFallbackModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationPrimaryProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationPrimaryProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationPrimaryModelIdProperty = SettingProperty<String>(\n key: _keyTranslationPrimaryModelId,\n defaultValue: defaultTranslationPrimaryModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n ),\n );\n\n late final _translationFallbackProviderProperty =\n SettingProperty<LyricsAiProvider>(\n key: _keyTranslationFallbackProvider,\n defaultValue: LyricsAiProvider.googleAiStudio,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialLyricsProvider(\n prefs,\n key: key,\n legacyKey: _legacyKeyLyricsAiProvider,\n defaultValue: def,\n ),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _translationFallbackModelIdProperty = SettingProperty<String>(\n key: _keyTranslationFallbackModelId,\n defaultValue: defaultTranslationFallbackModelId,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => _initialModelId(\n prefs,\n key: key,\n legacyKey: _legacyKeyGeminiTranslationModelId,\n defaultValue: def,\n emptyUsesDefault: false,\n ),\n );\n\n late final _geminiApiKeyProperty = SettingProperty<String>(\n key: geminiApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _openRouterApiKeyProperty = SettingProperty<String>(\n key: openRouterApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _doubaoApiKeyProperty = SettingProperty<String>(\n key: doubaoApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _deepseekApiKeyProperty = SettingProperty<String>(\n key: deepseekApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderApiKeyProperty = SettingProperty<String>(\n key: customProviderApiKeyStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderBaseUrlProperty = SettingProperty<String>(\n key: customProviderBaseUrlStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n },\n );\n\n late final _customProviderNameProperty = SettingProperty<String>(\n key: customProviderNameStorageKey,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n final normalized = val.trim();\n if (normalized.isEmpty) {\n prefs.remove(key);\n } else {\n prefs.setString(key, normalized);\n }\n _lastKnownCustomProviderName = normalized;\n },\n );\n\n late final _visualizerColorProperty = SettingProperty<Color>(\n key: _keyVisColor,\n defaultValue: Colors.white,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerOpacityProperty = SettingProperty<double>(\n key: _keyVisOpacity,\n defaultValue: 0.2,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerGradientEnabledProperty = SettingProperty<bool>(\n key: _keyVisGradient,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerStartColorProperty = SettingProperty<Color>(\n key: _keyVisStartColor,\n defaultValue: Colors.blue,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerEndColorProperty = SettingProperty<Color>(\n key: _keyVisEndColor,\n defaultValue: Colors.purple,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) => Color(prefs.getInt(key) ?? def.toARGB32()),\n customWrite: (prefs, key, val) => prefs.setInt(key, val.toARGB32()),\n );\n\n late final _visualizerGradientStop1Property = SettingProperty<double>(\n key: _keyVisGradientStop1,\n defaultValue: 0.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientStop2Property = SettingProperty<double>(\n key: _keyVisGradientStop2,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _visualizerGradientTileModeProperty = SettingProperty<int>(\n key: _keyVisGradientTileMode,\n defaultValue: TileMode.clamp.index,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicStartColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicStartColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isVisualizerDynamicEndColorProperty = SettingProperty<bool>(\n key: _keyVisualizerDynamicEndColor,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundTypeProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundType,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackRadialGradientEnabledProperty = SettingProperty<bool>(\n key: _keyPlaybackRadialGradientEnabled,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundColorProperty = SettingProperty<int>(\n key: _keyPlaybackBackgroundColor,\n defaultValue: 0xFF1A1F2C,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundCustomImagePathProperty =\n SettingProperty<String>(\n key: _keyPlaybackBackgroundCustomImagePath,\n defaultValue: '',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundNormalOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundNormalOpacity,\n defaultValue: defaultPlaybackBackgroundNormalOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBackgroundLyricsOpacityProperty = SettingProperty<double>(\n key: _keyPlaybackBackgroundLyricsOpacity,\n defaultValue: defaultPlaybackBackgroundLyricsOpacity,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackBlurredArtworkBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackBlurredArtworkBlurSigma,\n defaultValue: defaultPlaybackBlurredArtworkBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackCustomImageBlurSigmaProperty = SettingProperty<double>(\n key: _keyPlaybackCustomImageBlurSigma,\n defaultValue: defaultPlaybackCustomImageBlurSigma,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackMeshBackgroundSpeedProperty = SettingProperty<double>(\n key: _keyPlaybackMeshBackgroundSpeed,\n defaultValue: 0.05,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isAutoModeProperty = SettingProperty<bool>(\n key: _keyIsAutoMode,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpectrumQuantityProperty = SettingProperty<String>(\n key: _keyAutoSpectrumQuantity,\n defaultValue: 'high',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _autoSpeedProperty = SettingProperty<String>(\n key: _keyAutoSpeed,\n defaultValue: 'medium',\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyPortraitFrequencyGroups,\n defaultValue: 100,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeFrequencyGroupsProperty = SettingProperty<int>(\n key: _keyLandscapeFrequencyGroups,\n defaultValue: 172,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _portraitGapProperty = SettingProperty<double>(\n key: _keyPortraitGap,\n defaultValue: 1.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _landscapeGapProperty = SettingProperty<double>(\n key: _keyLandscapeGap,\n defaultValue: 2.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _isWaveformProgressBarEnabledProperty = SettingProperty<bool>(\n key: _keyIsWaveformProgressBarEnabled,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _waveformLongPressSeekSpeedProperty = SettingProperty<double>(\n key: _keyWaveformLongPressSeekSpeed,\n defaultValue: defaultWaveformLongPressSeekSpeed,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setDouble(\n key,\n val.clamp(\n minWaveformLongPressSeekSpeed,\n maxWaveformLongPressSeekSpeed,\n ),\n );\n },\n );\n\n late final _enableWaveformLongPressSeekProperty = SettingProperty<bool>(\n key: _keyEnableWaveformLongPressSeek,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _playbackSpeedLimit5xProperty = SettingProperty<bool>(\n key: _keyPlaybackSpeedLimit5x,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _enableFadeEffectProperty = SettingProperty<bool>(\n key: _keyEnableFadeEffect,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _equalizerBandCountProperty = SettingProperty<int>(\n key: _keyEqualizerBandCount,\n defaultValue: 10,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _showDeveloperOptionsProperty = SettingProperty<bool>(\n key: _keyShowDeveloperOptions,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanEnabledProperty = SettingProperty<bool>(\n key: skipShortAudioScanEnabledStorageKey,\n defaultValue: false,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _skipShortAudioScanMinimumDurationSecondsProperty =\n SettingProperty<int>(\n key: skipShortAudioScanMinimumDurationSecondsStorageKey,\n defaultValue: defaultSkipShortAudioScanMinimumDurationSeconds,\n prefs: _prefs,\n onChanged: notifyListeners,\n customWrite: (prefs, key, val) {\n prefs.setInt(key, val.clamp(1, 3600).toInt());\n },\n );\n\n late final _randomRangeProperty = SettingProperty<int>(\n key: _keyRandomRange,\n defaultValue: 0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _randomMethodProperty = SettingProperty<int>(\n key: _keyRandomMethod,\n defaultValue: 1, // Default to shuffle\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _transcodeDefaultFormatProperty = SettingProperty<AudioFormat>(\n key: _keyTranscodeDefaultFormat,\n defaultValue: AudioFormat.m4a,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n _audioFormatFromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) => prefs.setString(key, val.value),\n );\n\n late final _transcodeDefaultQualityTierProperty =\n SettingProperty<TranscodeQualityTier>(\n key: _keyTranscodeDefaultQualityTier,\n defaultValue: TranscodeQualityTier.medium,\n prefs: _prefs,\n onChanged: notifyListeners,\n customRead: (prefs, key, def) =>\n TranscodeQualityTierX.fromStorageValue(prefs.getString(key)),\n customWrite: (prefs, key, val) =>\n prefs.setString(key, val.storageValue),\n );\n\n late final _smallWindowWidthProperty = SettingProperty<double>(\n key: _keySmallWindowWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowHeightProperty = SettingProperty<double>(\n key: _keySmallWindowHeight,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowBottomPanelModeProperty = SettingProperty<String>(\n key: _keySmallWindowBottomPanelMode,\n defaultValue: SmallWindowBottomPanelMode.collapsed.name,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowAlwaysOnTopProperty = SettingProperty<bool>(\n key: _keySmallWindowAlwaysOnTop,\n defaultValue: true,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueWidthProperty = SettingProperty<double>(\n key: _keySmallWindowQueueWidth,\n defaultValue: 360.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n late final _smallWindowQueueHeightProperty = SettingProperty<double>(\n key: _keySmallWindowQueueHeight,\n defaultValue: 600.0,\n prefs: _prefs,\n onChanged: notifyListeners,\n );\n\n static LyricsAiProvider _initialLyricsProvider(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required LyricsAiProvider defaultValue,\n }) {\n final stored = prefs.getString(key);\n if (stored != null && stored.trim().isNotEmpty) {\n return LyricsAiProviderX.fromStorageValue(stored);\n }\n return LyricsAiProviderX.fromStorageValue(prefs.getString(legacyKey));\n }\n\n static String _initialModelId(\n SharedPreferences prefs, {\n required String key,\n required String legacyKey,\n required String defaultValue,\n bool emptyUsesDefault = true,\n }) {\n final normalized = prefs.getString(key)?.trim();\n if (normalized != null) {\n if (normalized.isEmpty && !emptyUsesDefault) {\n return '';\n }\n if (normalized.isNotEmpty) {\n return normalized;\n }\n }\n\n final legacy = prefs.getString(legacyKey)?.trim();\n if (legacy != null && legacy.isNotEmpty) {\n return legacy;\n }\n\n return emptyUsesDefault ? defaultValue : '';\n }\n\n SettingsService(this._prefs)\n : _shortcutBindings = _loadShortcutBindings(_prefs) {\n _lastKnownCustomProviderName =\n _prefs.getString(customProviderNameStorageKey)?.trim() ?? '';\n LocalizedText.overrideLanguageCode = _prefs.getString(_keyLocale) ?? 'system';\n }\n\n bool get hasShownOnboarding => _hasShownOnboardingProperty.value;\n set hasShownOnboarding(bool value) =>\n _hasShownOnboardingProperty.value = value;\n\n bool get hasShownCoverTapLyricTip => _hasShownCoverTapLyricTipProperty.value;\n set hasShownCoverTapLyricTip(bool value) =>\n _hasShownCoverTapLyricTipProperty.value = value;\n\n bool get hasShownLyricsMenuTip => _hasShownLyricsMenuTipProperty.value;\n set hasShownLyricsMenuTip(bool value) =>\n _hasShownLyricsMenuTipProperty.value = value;\n\n bool get tagCompletionSaveToSourceFile =>\n _tagCompletionSaveToSourceFileProperty.value;\n set tagCompletionSaveToSourceFile(bool value) =>\n _tagCompletionSaveToSourceFileProperty.value = value;\n\n bool get lanSharingEnabled => _lanSharingEnabledProperty.value;\n set lanSharingEnabled(bool value) => _lanSharingEnabledProperty.value = value;\n\n String get lanSharingFolderPath => _lanSharingFolderPathProperty.value;\n set lanSharingFolderPath(String value) =>\n _lanSharingFolderPathProperty.value = value;\n\n bool get hasLanSharingFolderPath => lanSharingFolderPath.trim().isNotEmpty;\n\n ThemeMode get themeMode => _themeModeProperty.value;\n set themeMode(ThemeMode value) => _themeModeProperty.value = value;\n\n double get uiScale => _uiScaleProperty.value;\n set uiScale(double value) =>\n _uiScaleProperty.value = value.clamp(minUiScale, maxUiScale);\n\n String get appLocale => _localeProperty.value;\n set appLocale(String value) => _localeProperty.value = value;\n\n Locale? get effectiveLocale {\n final stored = appLocale;\n if (stored == 'system' || stored.isEmpty) {\n return null;\n }\n if (stored == 'zh_Hant') {\n return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant');\n }\n return Locale(stored);\n }\n\n bool get isImmersiveTabBarEnabled => _isImmersiveTabBarEnabledProperty.value;\n set isImmersiveTabBarEnabled(bool value) =>\n _isImmersiveTabBarEnabledProperty.value = value;\n\n bool get collapseButtonsInLandscapeLyrics =>\n _collapseButtonsInLandscapeLyricsProperty.value;\n set collapseButtonsInLandscapeLyrics(bool value) =>\n _collapseButtonsInLandscapeLyricsProperty.value = value;\n\n List<String> get topButtonsOrder => _topButtonsOrderProperty.value;\n set topButtonsOrder(List<String> value) {\n _topButtonsOrderProperty.value = value;\n }\n\n String get mainControlsLeftButton => _mainControlsLeftButtonProperty.value;\n set mainControlsLeftButton(String value) {\n _mainControlsLeftButtonProperty.value = value;\n }\n\n String get mainControlsRightButton => _mainControlsRightButtonProperty.value;\n set mainControlsRightButton(String value) {\n _mainControlsRightButtonProperty.value = value;\n }\n\n String get lyricsHeaderRightButton => _lyricsHeaderRightButtonProperty.value;\n set lyricsHeaderRightButton(String value) {\n _lyricsHeaderRightButtonProperty.value = value;\n }\n\n void resetPlaybackButtonsToDefault() {\n topButtonsOrder = List<String>.from(defaultTopButtonsOrder);\n mainControlsLeftButton = defaultMainControlsLeftButton;\n mainControlsRightButton = defaultMainControlsRightButton;\n lyricsHeaderRightButton = defaultLyricsHeaderRightButton;\n }\n\n bool get showScanProgressToast => _showScanProgressToastProperty.value;\n set showScanProgressToast(bool value) =>\n _showScanProgressToastProperty.value = value;\n\n bool get openPlaybackOnDirectorySongTap =>\n _openPlaybackOnDirectorySongTapProperty.value;\n set openPlaybackOnDirectorySongTap(bool value) =>\n _openPlaybackOnDirectorySongTapProperty.value = value;\n\n bool get defaultToLyricsModeOnPlaybackOpen =>\n _defaultToLyricsModeOnPlaybackOpenProperty.value;\n set defaultToLyricsModeOnPlaybackOpen(bool value) =>\n _defaultToLyricsModeOnPlaybackOpenProperty.value = value;\n\n bool get windowsAutoRepairShortcut => _windowsAutoRepairShortcutProperty.value;\n set windowsAutoRepairShortcut(bool value) =>\n _windowsAutoRepairShortcutProperty.value = value;\n\n bool get enableSystemTray => _enableSystemTrayProperty.value;\n set enableSystemTray(bool value) =>\n _enableSystemTrayProperty.value = value;\n\n CloseWindowAction get closeWindowAction => _closeWindowActionProperty.value;\n set closeWindowAction(CloseWindowAction value) {\n _closeWindowActionProperty.value = value;\n _closeToTrayProperty.value = (value == CloseWindowAction.minimize);\n }\n\n bool get closeToTray => closeWindowAction == CloseWindowAction.minimize;\n set closeToTray(bool value) {\n closeWindowAction = value ? CloseWindowAction.minimize : CloseWindowAction.exit;\n }\n\n int get sampleStride => _sampleStrideProperty.value;\n set sampleStride(int value) => _sampleStrideProperty.value = value;\n\n int get waveformChunks => _waveformChunksProperty.value;\n set waveformChunks(int value) => _waveformChunksProperty.value = value;\n\n bool get isUserInactive => _isUserInactive;\n set isUserInactive(bool value) {\n if (_isUserInactive != value) {\n _isUserInactive = value;\n notifyListeners();\n }\n if (!value) {\n startInactivityTimer();\n }\n }\n\n String get lyricsTranslationTargetLanguageCode =>\n _lyricsTranslationTargetLanguageProperty.value;\n set lyricsTranslationTargetLanguageCode(String value) =>\n _lyricsTranslationTargetLanguageProperty.value = value;\n\n String get effectiveLyricsTranslationTargetLanguageCode {\n final stored = lyricsTranslationTargetLanguageCode.trim();\n if (stored.isNotEmpty) {\n return stored;\n }\n return LanguageCodeUtils.currentAppLanguageCode();\n }\n\n\n LyricsSaveMethod get lyricsSaveMethod {\n return LyricsSaveMethod.values.firstWhere(\n (method) => method.name == _lyricsSaveMethodProperty.value,\n orElse: () => LyricsSaveMethod.original,\n );\n }\n\n set lyricsSaveMethod(LyricsSaveMethod value) {\n _lyricsSaveMethodProperty.value = value.name;\n }\n\n LyricsStyle get lyricsStyle {\n return LyricsStyle.values.firstWhere(\n (style) => style.name == _lyricsStyleProperty.value,\n orElse: () => LyricsStyle.apple,\n );\n }\n\n set lyricsStyle(LyricsStyle value) {\n _lyricsStyleProperty.value = value.name;\n }\n\n double get lyricsFontScale => lyricsFontScaleTraditional;\n set lyricsFontScale(double value) => lyricsFontScaleTraditional = value;\n\n double get lyricsFontScaleTraditional => _lyricsFontScaleTraditionalProperty.value;\n set lyricsFontScaleTraditional(double value) => _lyricsFontScaleTraditionalProperty.value = value;\n\n double get lyricsFontScaleApple => _lyricsFontScaleAppleProperty.value;\n set lyricsFontScaleApple(double value) => _lyricsFontScaleAppleProperty.value = value;\n\n LyricsAiProvider get lyricsAiProvider => generationPrimaryModel.provider;\n set lyricsAiProvider(LyricsAiProvider value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(provider: value);\n }\n\n bool get isLyricsAiAutoSwitchEnabled => false;\n set isLyricsAiAutoSwitchEnabled(bool value) {}\n\n LyricsAiModelSelection get generationPrimaryModel => LyricsAiModelSelection(\n provider: _generationPrimaryProviderProperty.value,\n modelId: _generationPrimaryModelIdProperty.value,\n );\n set generationPrimaryModel(LyricsAiModelSelection value) {\n _generationPrimaryProviderProperty.value = value.provider;\n _generationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get generationFallbackModel => LyricsAiModelSelection(\n provider: _generationFallbackProviderProperty.value,\n modelId: _generationFallbackModelIdProperty.value,\n );\n set generationFallbackModel(LyricsAiModelSelection value) {\n _generationFallbackProviderProperty.value = value.provider;\n _generationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationPrimaryModel => LyricsAiModelSelection(\n provider: _translationPrimaryProviderProperty.value,\n modelId: _translationPrimaryModelIdProperty.value,\n );\n set translationPrimaryModel(LyricsAiModelSelection value) {\n _translationPrimaryProviderProperty.value = value.provider;\n _translationPrimaryModelIdProperty.value = value.modelId.trim();\n }\n\n LyricsAiModelSelection get translationFallbackModel => LyricsAiModelSelection(\n provider: _translationFallbackProviderProperty.value,\n modelId: _translationFallbackModelIdProperty.value,\n );\n set translationFallbackModel(LyricsAiModelSelection value) {\n _translationFallbackProviderProperty.value = value.provider;\n _translationFallbackModelIdProperty.value = value.modelId.trim();\n }\n\n String get geminiApiKey => _geminiApiKeyProperty.value;\n set geminiApiKey(String value) {\n _geminiApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.googleAiStudio);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.googleAiStudio);\n }\n }\n\n String get openRouterApiKey => _openRouterApiKeyProperty.value;\n set openRouterApiKey(String value) {\n _openRouterApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.openRouter);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.openRouter);\n }\n }\n\n String get doubaoApiKey => _doubaoApiKeyProperty.value;\n set doubaoApiKey(String value) {\n _doubaoApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.doubao);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.doubao);\n }\n }\n\n String get deepseekApiKey => _deepseekApiKeyProperty.value;\n set deepseekApiKey(String value) {\n _deepseekApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.deepseek);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.deepseek);\n }\n }\n\n String get customProviderApiKey => _customProviderApiKeyProperty.value;\n set customProviderApiKey(String value) {\n _customProviderApiKeyProperty.value = value;\n if (value.trim().isEmpty) {\n _cleanupModelSelectionsForProvider(LyricsAiProvider.custom);\n } else {\n _setDefaultModelIfMissing(LyricsAiProvider.custom);\n }\n }\n\n String get customProviderBaseUrl => _customProviderBaseUrlProperty.value;\n set customProviderBaseUrl(String value) {\n _customProviderBaseUrlProperty.value = value;\n }\n\n String get customProviderName => _customProviderNameProperty.value;\n set customProviderName(String value) {\n _customProviderNameProperty.value = value;\n }\n\n bool _isModelSelectionNotSet(LyricsAiModelSelection selection) {\n return selection.modelId.trim().isEmpty ||\n apiKeyForProvider(selection.provider).trim().isEmpty;\n }\n\n void _setDefaultModelIfMissing(LyricsAiProvider provider) {\n if (_isModelSelectionNotSet(generationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultGenerationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterGenerationModelId,\n LyricsAiProvider.doubao => defaultDoubaoGenerationModelId,\n LyricsAiProvider.deepseek => '',\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n\n if (_isModelSelectionNotSet(translationPrimaryModel)) {\n final defaultModelId = switch (provider) {\n LyricsAiProvider.googleAiStudio => defaultTranslationPrimaryModelId,\n LyricsAiProvider.openRouter => defaultOpenRouterTranslationModelId,\n LyricsAiProvider.doubao => defaultDoubaoTranslationModelId,\n LyricsAiProvider.deepseek => defaultDeepSeekTranslationModelId,\n LyricsAiProvider.custom => '',\n };\n if (defaultModelId.isNotEmpty) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: provider,\n modelId: defaultModelId,\n );\n }\n }\n }\n\n void _cleanupModelSelectionsForProvider(LyricsAiProvider provider) {\n final remainingProviders = LyricsAiProvider.values\n .where((p) => p != provider && apiKeyForProvider(p).trim().isNotEmpty)\n .toList();\n\n final fallbackProvider = remainingProviders.isNotEmpty\n ? remainingProviders.first\n : LyricsAiProvider.googleAiStudio;\n\n if (generationPrimaryModel.provider == provider) {\n generationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (generationFallbackModel.provider == provider) {\n generationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationPrimaryModel.provider == provider) {\n translationPrimaryModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n if (translationFallbackModel.provider == provider) {\n translationFallbackModel = LyricsAiModelSelection(\n provider: fallbackProvider,\n modelId: '',\n );\n }\n }\n\n bool get hasCustomGoogleAiStudioApiKey =>\n _prefs.containsKey(geminiApiKeyStorageKey);\n bool get hasCustomOpenRouterApiKey =>\n _prefs.containsKey(openRouterApiKeyStorageKey);\n bool get hasCustomDoubaoApiKey => _prefs.containsKey(doubaoApiKeyStorageKey);\n bool get hasCustomDeepSeekApiKey =>\n _prefs.containsKey(deepseekApiKeyStorageKey);\n bool get hasCustomProviderConfigured =>\n _prefs.containsKey(customProviderApiKeyStorageKey);\n bool get hasBothLyricsGenerationApiKeys =>\n geminiApiKey.trim().isNotEmpty &&\n openRouterApiKey.trim().isNotEmpty &&\n doubaoApiKey.trim().isNotEmpty;\n bool get canAutoSwitchLyricsProvider => false;\n bool get shouldAutoSwitchLyricsProvider => false;\n String get activeLyricsGenerationApiKey =>\n apiKeyForProvider(generationPrimaryModel.provider);\n String get activeLyricsApiKey => activeLyricsGenerationApiKey;\n bool get hasActiveLyricsGenerationApiKey =>\n activeLyricsGenerationApiKey.trim().isNotEmpty;\n bool get hasActiveLyricsApiKey => hasActiveLyricsGenerationApiKey;\n String get activeGeminiTranslationApiKey => geminiApiKey;\n bool get hasGeminiTranslationApiKey => geminiApiKey.trim().isNotEmpty;\n\n String get geminiPrimaryModelId => generationPrimaryModel.modelId;\n set geminiPrimaryModelId(String value) {\n generationPrimaryModel = generationPrimaryModel.copyWith(modelId: value);\n }\n\n String get geminiFallbackModelId => generationFallbackModel.modelId;\n set geminiFallbackModelId(String value) {\n generationFallbackModel = generationFallbackModel.copyWith(modelId: value);\n }\n\n String get geminiTranslationModelId => translationPrimaryModel.modelId;\n set geminiTranslationModelId(String value) {\n translationPrimaryModel = translationPrimaryModel.copyWith(modelId: value);\n }\n\n String apiKeyForProvider(LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => geminiApiKey,\n LyricsAiProvider.openRouter => openRouterApiKey,\n LyricsAiProvider.doubao => doubaoApiKey,\n LyricsAiProvider.deepseek => deepseekApiKey,\n LyricsAiProvider.custom => customProviderApiKey,\n };\n }\n\n bool hasApiKeyForProvider(LyricsAiProvider provider) {\n return apiKeyForProvider(provider).trim().isNotEmpty;\n }\n\n List<LyricsAiProvider> get availableLyricsModelProviders {\n return LyricsAiProvider.values\n .where(hasApiKeyForProvider)\n .toList(growable: false);\n }\n\n bool get hasAnyLyricsModelProvider =>\n availableLyricsModelProviders.isNotEmpty;\n\n bool get hasCustomAcoustidApiKey =>\n _prefs.containsKey(acoustidApiKeyStorageKey);\n ShortcutBinding shortcutBinding(AppShortcutAction action) {\n return _shortcutBindings[action.storageKey] ?? action.defaultBinding;\n }\n\n Map<AppShortcutAction, ShortcutBinding> get shortcutBindings {\n return {\n for (final action in AppShortcutAction.values)\n action: shortcutBinding(action),\n };\n }\n\n static String lyricsModelDisplayName(String modelId) {\n final trimmed = modelId.trim();\n if (trimmed.isEmpty) {\n return _l10n().noModelSelected;\n }\n\n final normalized = trimmed.startsWith('google/')\n ? trimmed.substring('google/'.length)\n : trimmed;\n return normalized\n .split('-')\n .map((word) {\n if (word.isEmpty) return '';\n return word[0].toUpperCase() + word.substring(1);\n })\n .join(' ');\n }\n\n static String lyricsModelSelectionLabel(LyricsAiModelSelection selection) {\n final providerName = selection.provider.displayName;\n final modelName = lyricsModelDisplayName(selection.modelId);\n if (selection.modelId.trim().isEmpty) {\n return modelName;\n }\n return '$providerName · $modelName';\n }\n\n String get acoustidApiKey {\n final stored = _prefs.getString(acoustidApiKeyStorageKey)?.trim();\n if (stored != null && stored.isNotEmpty) {\n return stored;\n }\n return _builtInAcoustidApiKey;\n }\n\n Color get visualizerColor => _visualizerColorProperty.value;\n set visualizerColor(Color value) => _visualizerColorProperty.value = value;\n\n double get visualizerOpacity => _visualizerOpacityProperty.value;\n set visualizerOpacity(double value) =>\n _visualizerOpacityProperty.value = value;\n\n bool get isVisualizerGradientEnabled =>\n _isVisualizerGradientEnabledProperty.value;\n set isVisualizerGradientEnabled(bool value) =>\n _isVisualizerGradientEnabledProperty.value = value;\n\n Color get visualizerStartColor => _visualizerStartColorProperty.value;\n set visualizerStartColor(Color value) =>\n _visualizerStartColorProperty.value = value;\n\n Color get visualizerEndColor => _visualizerEndColorProperty.value;\n set visualizerEndColor(Color value) =>\n _visualizerEndColorProperty.value = value;\n\n double get visualizerGradientStop1 => _visualizerGradientStop1Property.value;\n set visualizerGradientStop1(double value) =>\n _visualizerGradientStop1Property.value = value;\n\n double get visualizerGradientStop2 => _visualizerGradientStop2Property.value;\n set visualizerGradientStop2(double value) =>\n _visualizerGradientStop2Property.value = value;\n\n int get visualizerGradientTileMode =>\n _visualizerGradientTileModeProperty.value;\n set visualizerGradientTileMode(int value) =>\n _visualizerGradientTileModeProperty.value = value;\n\n bool get isVisualizerDynamicColor => _isVisualizerDynamicColorProperty.value;\n set isVisualizerDynamicColor(bool value) =>\n _isVisualizerDynamicColorProperty.value = value;\n\n bool get isVisualizerDynamicStartColor =>\n _isVisualizerDynamicStartColorProperty.value;\n set isVisualizerDynamicStartColor(bool value) =>\n _isVisualizerDynamicStartColorProperty.value = value;\n\n bool get isVisualizerDynamicEndColor =>\n _isVisualizerDynamicEndColorProperty.value;\n set isVisualizerDynamicEndColor(bool value) =>\n _isVisualizerDynamicEndColorProperty.value = value;\n\n int get playbackBackgroundType => _playbackBackgroundTypeProperty.value;\n set playbackBackgroundType(int value) =>\n _playbackBackgroundTypeProperty.value = value;\n\n bool get playbackRadialGradientEnabled =>\n _playbackRadialGradientEnabledProperty.value;\n set playbackRadialGradientEnabled(bool value) =>\n _playbackRadialGradientEnabledProperty.value = value;\n\n int get playbackBackgroundColor => _playbackBackgroundColorProperty.value;\n set playbackBackgroundColor(int value) =>\n _playbackBackgroundColorProperty.value = value;\n\n String get playbackBackgroundCustomImagePath =>\n _playbackBackgroundCustomImagePathProperty.value;\n set playbackBackgroundCustomImagePath(String value) =>\n _playbackBackgroundCustomImagePathProperty.value = value;\n\n double get playbackBackgroundNormalOpacity =>\n _playbackBackgroundNormalOpacityProperty.value;\n set playbackBackgroundNormalOpacity(double value) =>\n _playbackBackgroundNormalOpacityProperty.value = value;\n\n double get playbackBackgroundLyricsOpacity =>\n _playbackBackgroundLyricsOpacityProperty.value;\n set playbackBackgroundLyricsOpacity(double value) =>\n _playbackBackgroundLyricsOpacityProperty.value = value;\n\n double get playbackBlurredArtworkBlurSigma =>\n defaultPlaybackBlurredArtworkBlurSigma;\n set playbackBlurredArtworkBlurSigma(double value) =>\n _playbackBlurredArtworkBlurSigmaProperty.value = value;\n\n double get playbackCustomImageBlurSigma =>\n defaultPlaybackCustomImageBlurSigma;\n set playbackCustomImageBlurSigma(double value) =>\n _playbackCustomImageBlurSigmaProperty.value = value;\n\n double get playbackMeshBackgroundSpeed =>\n _playbackMeshBackgroundSpeedProperty.value;\n set playbackMeshBackgroundSpeed(double value) =>\n _playbackMeshBackgroundSpeedProperty.value = value;\n\n bool get isAutoMode => _isAutoModeProperty.value;\n set isAutoMode(bool value) => _isAutoModeProperty.value = value;\n\n String get autoSpectrumQuantity => _autoSpectrumQuantityProperty.value;\n set autoSpectrumQuantity(String value) =>\n _autoSpectrumQuantityProperty.value = value;\n\n String get autoSpeed => _autoSpeedProperty.value;\n set autoSpeed(String value) => _autoSpeedProperty.value = value;\n\n int get portraitFrequencyGroups => _portraitFrequencyGroupsProperty.value;\n set portraitFrequencyGroups(int value) =>\n _portraitFrequencyGroupsProperty.value = value;\n\n int get landscapeFrequencyGroups => _landscapeFrequencyGroupsProperty.value;\n set landscapeFrequencyGroups(int value) =>\n _landscapeFrequencyGroupsProperty.value = value;\n\n double get portraitGap => _portraitGapProperty.value;\n set portraitGap(double value) => _portraitGapProperty.value = value;\n\n double get landscapeGap => _landscapeGapProperty.value;\n set landscapeGap(double value) => _landscapeGapProperty.value = value;\n\n bool get isWaveformProgressBarEnabled =>\n _isWaveformProgressBarEnabledProperty.value;\n set isWaveformProgressBarEnabled(bool value) =>\n _isWaveformProgressBarEnabledProperty.value = value;\n\n double get waveformLongPressSeekSpeed =>\n _waveformLongPressSeekSpeedProperty.value;\n set waveformLongPressSeekSpeed(double value) =>\n _waveformLongPressSeekSpeedProperty.value = value;\n\n bool get enableWaveformLongPressSeek =>\n _enableWaveformLongPressSeekProperty.value;\n set enableWaveformLongPressSeek(bool value) =>\n _enableWaveformLongPressSeekProperty.value = value;\n\n bool get playbackSpeedLimit5x =>\n _playbackSpeedLimit5xProperty.value;\n set playbackSpeedLimit5x(bool value) =>\n _playbackSpeedLimit5xProperty.value = value;\n\n bool get enableFadeEffect => _enableFadeEffectProperty.value;\n set enableFadeEffect(bool value) =>\n _enableFadeEffectProperty.value = value;\n\n int get equalizerBandCount => _equalizerBandCountProperty.value;\n set equalizerBandCount(int value) =>\n _equalizerBandCountProperty.value = value;\n\n bool get showDeveloperOptions => _showDeveloperOptionsProperty.value;\n set showDeveloperOptions(bool value) =>\n _showDeveloperOptionsProperty.value = value;\n\n bool get skipShortAudioScanEnabled =>\n _skipShortAudioScanEnabledProperty.value;\n set skipShortAudioScanEnabled(bool value) =>\n _skipShortAudioScanEnabledProperty.value = value;\n\n int get skipShortAudioScanMinimumDurationSeconds =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value;\n set skipShortAudioScanMinimumDurationSeconds(int value) =>\n _skipShortAudioScanMinimumDurationSecondsProperty.value = value;\n\n int get randomRange => _randomRangeProperty.value;\n set randomRange(int value) => _randomRangeProperty.value = value;\n\n int get randomMethod => _randomMethodProperty.value;\n set randomMethod(int value) => _randomMethodProperty.value = value;\n\n AudioFormat get transcodeDefaultFormat =>\n _transcodeDefaultFormatProperty.value;\n set transcodeDefaultFormat(AudioFormat value) =>\n _transcodeDefaultFormatProperty.value = value;\n\n TranscodeQualityTier get transcodeDefaultQualityTier =>\n _transcodeDefaultQualityTierProperty.value;\n set transcodeDefaultQualityTier(TranscodeQualityTier value) =>\n _transcodeDefaultQualityTierProperty.value = value;\n\n double get smallWindowWidth => _smallWindowWidthProperty.value;\n set smallWindowWidth(double value) => _smallWindowWidthProperty.value = value;\n\n double get smallWindowHeight => _smallWindowHeightProperty.value;\n set smallWindowHeight(double value) =>\n _smallWindowHeightProperty.value = value;\n\n double get smallWindowQueueWidth => _smallWindowQueueWidthProperty.value;\n set smallWindowQueueWidth(double value) =>\n _smallWindowQueueWidthProperty.value = value;\n\n double get smallWindowQueueHeight => _smallWindowQueueHeightProperty.value;\n set smallWindowQueueHeight(double value) =>\n _smallWindowQueueHeightProperty.value = value;\n\n Size get savedSmallWindowSize => Size(smallWindowWidth, smallWindowHeight);\n set savedSmallWindowSize(Size size) {\n smallWindowWidth = size.width;\n smallWindowHeight = size.height;\n }\n\n Size get savedSmallWindowQueueSize =>\n Size(smallWindowQueueWidth, smallWindowQueueHeight);\n set savedSmallWindowQueueSize(Size size) {\n smallWindowQueueWidth = size.width;\n smallWindowQueueHeight = size.height;\n }\n\n FolderViewMode get folderViewMode => _folderViewModeProperty.value;\n set folderViewMode(FolderViewMode value) => _folderViewModeProperty.value = value;\n\n SharedPreferences get prefs => _prefs;\n\n static AudioFormat _audioFormatFromStorageValue(String? value) {\n final normalized = value?.trim().toLowerCase();\n if (normalized == null || normalized.isEmpty) {\n return AudioFormat.m4a;\n }\n try {\n return audioFormatFromValue(normalized);\n } catch (_) {\n return AudioFormat.m4a;\n }\n }\n\n static Map<String, ShortcutBinding> _loadShortcutBindings(\n SharedPreferences prefs,\n ) {\n final raw = prefs.getString(_keyShortcutBindings);\n if (raw == null || raw.trim().isEmpty) {\n return <String, ShortcutBinding>{};\n }\n\n try {\n final decoded = jsonDecode(raw);\n if (decoded is! Map) {\n return <String, ShortcutBinding>{};\n }\n\n final result = <String, ShortcutBinding>{};\n for (final entry in decoded.entries) {\n if (entry.key is! String) {\n continue;\n }\n final binding = ShortcutBinding.fromJson(entry.value);\n if (binding == null) {\n continue;\n }\n result[entry.key as String] = binding;\n }\n return result;\n } catch (_) {\n return <String, ShortcutBinding>{};\n }\n }\n\n Future<void> _saveShortcutBindings() async {\n if (_shortcutBindings.isEmpty) {\n await _prefs.remove(_keyShortcutBindings);\n return;\n }\n\n final encoded = jsonEncode({\n for (final entry in _shortcutBindings.entries)\n entry.key: entry.value.toJson(),\n });\n await _prefs.setString(_keyShortcutBindings, encoded);\n }\n\n void resetVisualizerAppearance() {\n _visualizerOpacityProperty.reset();\n _visualizerColorProperty.reset();\n _isVisualizerGradientEnabledProperty.reset();\n _visualizerStartColorProperty.reset();\n _visualizerEndColorProperty.reset();\n _visualizerGradientStop1Property.reset();\n _visualizerGradientStop2Property.reset();\n _visualizerGradientTileModeProperty.reset();\n _isVisualizerDynamicColorProperty.reset();\n _isVisualizerDynamicStartColorProperty.reset();\n _isVisualizerDynamicEndColorProperty.reset();\n _playbackMeshBackgroundSpeedProperty.reset();\n _playbackBackgroundColorProperty.reset();\n _playbackRadialGradientEnabledProperty.reset();\n _playbackBackgroundCustomImagePathProperty.reset();\n _playbackBackgroundNormalOpacityProperty.reset();\n _playbackBackgroundLyricsOpacityProperty.reset();\n _playbackBlurredArtworkBlurSigmaProperty.reset();\n _playbackCustomImageBlurSigmaProperty.reset();\n _isAutoModeProperty.reset();\n _autoSpectrumQuantityProperty.reset();\n _autoSpeedProperty.reset();\n _portraitFrequencyGroupsProperty.reset();\n _landscapeFrequencyGroupsProperty.reset();\n _portraitGapProperty.reset();\n _landscapeGapProperty.reset();\n _isWaveformProgressBarEnabledProperty.reset();\n _showDeveloperOptionsProperty.reset();\n _randomRangeProperty.reset();\n _randomMethodProperty.reset();\n }\n\n void increaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale + lyricsFontScaleStep;\n }\n\n void decreaseLyricsFontScale() {\n lyricsFontScale = lyricsFontScale - lyricsFontScaleStep;\n }\n\n void resetLyricsFontScale() {\n resetLyricsFontScaleTraditional();\n }\n\n void resetLyricsFontScaleTraditional() {\n _lyricsFontScaleTraditionalProperty.reset();\n }\n\n void resetLyricsFontScaleApple() {\n _lyricsFontScaleAppleProperty.reset();\n }\n\n void resetLyricsAiModels() {\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationPrimaryModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultGenerationFallbackModelId,\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationPrimaryModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.googleAiStudio,\n modelId: defaultTranslationFallbackModelId,\n );\n }\n\n void applyProviderDefaults(LyricsAiProvider provider) {\n switch (provider) {\n case LyricsAiProvider.googleAiStudio:\n resetLyricsAiModels();\n break;\n case LyricsAiProvider.openRouter:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: defaultOpenRouterTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.openRouter,\n modelId: '',\n );\n break;\n case LyricsAiProvider.doubao:\n generationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoGenerationModelId,\n );\n generationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: defaultDoubaoTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.doubao,\n modelId: '',\n );\n break;\n case LyricsAiProvider.deepseek:\n translationPrimaryModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: defaultDeepSeekTranslationModelId,\n );\n translationFallbackModel = const LyricsAiModelSelection(\n provider: LyricsAiProvider.deepseek,\n modelId: '',\n );\n break;\n case LyricsAiProvider.custom:\n break;\n }\n }\n\n void resetLyricsTranslationTargetLanguage() {\n _lyricsTranslationTargetLanguageProperty.reset();\n }\n\n void setShortcutBinding(AppShortcutAction action, ShortcutBinding binding) {\n _shortcutBindings[action.storageKey] = binding;\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void setShortcutBindings(Map<AppShortcutAction, ShortcutBinding> bindings) {\n _shortcutBindings = {\n for (final entry in bindings.entries) entry.key.storageKey: entry.value,\n };\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n void resetShortcutBindings() {\n _shortcutBindings = <String, ShortcutBinding>{};\n unawaited(_saveShortcutBindings());\n notifyListeners();\n }\n\n set acoustidApiKey(String value) {\n final normalized = value.trim();\n if (acoustidApiKey == normalized) {\n return;\n }\n\n if (normalized.isEmpty) {\n _prefs.remove(acoustidApiKeyStorageKey);\n } else {\n _prefs.setString(acoustidApiKeyStorageKey, normalized);\n }\n notifyListeners();\n }\n\n void startInactivityTimer() {\n _inactivityTimer?.cancel();\n _inactivityTimer = Timer(const Duration(seconds: 3), () {\n if (!_isUserInactive) {\n _isUserInactive = true;\n notifyListeners();\n }\n });\n }\n\n void resetInactivity() {\n if (_isUserInactive) {\n _isUserInactive = false;\n notifyListeners();\n }\n startInactivityTimer();\n }\n\n static Future<SettingsService> init() async {\n final prefs = await SharedPreferences.getInstance();\n return SettingsService(prefs);\n }\n\n static double _normalizeLyricsFontScale(double value) {\n final clamped = value.clamp(minLyricsFontScale, maxLyricsFontScale);\n return (clamped * 10).roundToDouble() / 10.0;\n }\n\n bool _isSmallWindowMode = false;\n bool get isSmallWindowMode => _isSmallWindowMode;\n set isSmallWindowMode(bool value) {\n if (_isSmallWindowMode != value) {\n _isSmallWindowMode = value;\n notifyListeners();\n }\n }\n\n bool get isSmallWindowAlwaysOnTop => _smallWindowAlwaysOnTopProperty.value;\n set isSmallWindowAlwaysOnTop(bool value) {\n _smallWindowAlwaysOnTopProperty.value = value;\n }\n\n SmallWindowBottomPanelMode get smallWindowBottomPanelMode {\n return SmallWindowBottomPanelMode.values.firstWhere(\n (mode) => mode.name == _smallWindowBottomPanelModeProperty.value,\n orElse: () => SmallWindowBottomPanelMode.collapsed,\n );\n }\n\n set smallWindowBottomPanelMode(SmallWindowBottomPanelMode value) {\n _smallWindowBottomPanelModeProperty.value = value.name;\n }\n\n void toggleSmallWindowBottomPanelMode(SmallWindowBottomPanelMode mode) {\n smallWindowBottomPanelMode = smallWindowBottomPanelMode == mode\n ? SmallWindowBottomPanelMode.collapsed\n : mode;\n }\n\n bool get isSmallWindowQueueExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue;\n set isSmallWindowQueueExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.queue;\n } else if (smallWindowBottomPanelMode == SmallWindowBottomPanelMode.queue) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowLyricsExpanded =>\n smallWindowBottomPanelMode == SmallWindowBottomPanelMode.lyrics;\n set isSmallWindowLyricsExpanded(bool value) {\n if (value) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.lyrics;\n } else if (smallWindowBottomPanelMode ==\n SmallWindowBottomPanelMode.lyrics) {\n smallWindowBottomPanelMode = SmallWindowBottomPanelMode.collapsed;\n }\n }\n\n bool get isSmallWindowBottomPanelExpanded =>\n smallWindowBottomPanelMode != SmallWindowBottomPanelMode.collapsed;\n\n Size get savedRegularWindowSize => Size(\n _regularWindowWidthProperty.value.clamp(400.0, 99999.0),\n _regularWindowHeightProperty.value.clamp(650.0, 99999.0),\n );\n\n set savedRegularWindowSize(Size size) {\n _regularWindowWidthProperty.value = size.width.clamp(400.0, 99999.0);\n _regularWindowHeightProperty.value = size.height.clamp(650.0, 99999.0);\n }\n}\n\nabstract final class LyricsModelRecommendation {\n static bool isGoogleRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n var baseId = lowerId;\n if (baseId.contains(':')) {\n baseId = baseId.split(':').first;\n }\n\n if (baseId.contains('image') || baseId.contains('tts')) {\n return false;\n }\n\n if (baseId == 'gemini-flash-latest' ||\n baseId == 'gemini-flash-lite-latest') {\n return true;\n }\n\n if (baseId.startsWith('gemma-')) {\n final match = RegExp(r'^gemma-(\\d+(?:\\.\\d+)?)-').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 4.0;\n }\n }\n\n if (baseId.contains('-flash-lite')) {\n final match = RegExp(\n r'gemini-(\\d+(?:\\.\\d+)?)-flash-lite',\n ).firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 3.1;\n }\n } else if (baseId.contains('-flash')) {\n final match = RegExp(r'gemini-(\\d+(?:\\.\\d+)?)-flash').firstMatch(baseId);\n if (match != null) {\n final ver = double.tryParse(match.group(1) ?? '');\n return ver != null && ver >= 2.5;\n }\n }\n\n return false;\n }\n\n static bool isOpenRouterRecommended(String modelId) {\n var id = modelId.toLowerCase();\n if (id.startsWith('google/')) {\n id = id.substring('google/'.length);\n } else if (id.startsWith('~google/')) {\n id = id.substring('~google/'.length);\n } else {\n return false;\n }\n return isGoogleRecommended(id);\n }\n\n static bool isDoubaoRecommended(String modelId) {\n final lowerId = modelId.toLowerCase();\n final regExp = RegExp(\n r'^doubao-seed-([0-9]+[\\.-][0-9]+)-(lite|mini|pro)(?:-.*)?$',\n );\n final match = regExp.firstMatch(lowerId);\n if (match == null) {\n return false;\n }\n\n final versionStr = match.group(1)!.replaceAll('-', '.');\n final version = double.tryParse(versionStr);\n if (version == null) {\n return false;\n }\n\n return version >= 2.0;\n }\n\n static bool isRecommended(String modelId, LyricsAiProvider provider) {\n return switch (provider) {\n LyricsAiProvider.googleAiStudio => isGoogleRecommended(modelId),\n LyricsAiProvider.openRouter => isOpenRouterRecommended(modelId),\n LyricsAiProvider.doubao => isDoubaoRecommended(modelId),\n LyricsAiProvider.deepseek => true,\n LyricsAiProvider.custom => true,\n };\n }\n}\n"}}}}