import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:plezy/utils/app_logger.dart'; enum ThemeMode { system, light, dark } enum LibraryDensity { compact, normal, comfortable } enum ViewMode { grid, list } class SettingsService { static const String _keyThemeMode = 'theme_mode'; static const String _keyEnableDebugLogging = 'enable_debug_logging'; static const String _keyBufferSize = 'buffer_size'; static const String _keyKeyboardShortcuts = 'keyboard_shortcuts'; static const String _keyKeyboardHotkeys = 'keyboard_hotkeys'; static const String _keyEnableHardwareDecoding = 'enable_hardware_decoding'; static const String _keyPreferredVideoCodec = 'preferred_video_codec'; static const String _keyPreferredAudioCodec = 'preferred_audio_codec'; static const String _keyLibraryDensity = 'library_density'; static const String _keyViewMode = 'view_mode'; static const String _keyUseSeasonPoster = 'use_season_poster'; static const String _keySeekTimeSmall = 'seek_time_small'; static const String _keySeekTimeLarge = 'seek_time_large'; static const String _keyMediaVersionPreferences = 'media_version_preferences'; static const String _keyShowHeroSection = 'show_hero_section'; static SettingsService? _instance; late SharedPreferences _prefs; SettingsService._(); static Future getInstance() async { if (_instance == null) { _instance = SettingsService._(); await _instance!._init(); } return _instance!; } Future _init() async { _prefs = await SharedPreferences.getInstance(); } // Theme Mode Future setThemeMode(ThemeMode mode) async { await _prefs.setString(_keyThemeMode, mode.name); } ThemeMode getThemeMode() { final modeString = _prefs.getString(_keyThemeMode); return ThemeMode.values.firstWhere( (mode) => mode.name == modeString, orElse: () => ThemeMode.system, ); } // Debug Logging Future setEnableDebugLogging(bool enabled) async { await _prefs.setBool(_keyEnableDebugLogging, enabled); // Update logger level immediately when setting changes setLoggerLevel(enabled); } bool getEnableDebugLogging() { return _prefs.getBool(_keyEnableDebugLogging) ?? false; } // Buffer Size (in MB) Future setBufferSize(int sizeInMB) async { await _prefs.setInt(_keyBufferSize, sizeInMB); } int getBufferSize() { return _prefs.getInt(_keyBufferSize) ?? 128; // Default 128MB } // Hardware Decoding Future setEnableHardwareDecoding(bool enabled) async { await _prefs.setBool(_keyEnableHardwareDecoding, enabled); } bool getEnableHardwareDecoding() { return _prefs.getBool(_keyEnableHardwareDecoding) ?? true; // Default enabled } // Preferred Video Codec Future setPreferredVideoCodec(String codec) async { await _prefs.setString(_keyPreferredVideoCodec, codec); } String getPreferredVideoCodec() { return _prefs.getString(_keyPreferredVideoCodec) ?? 'auto'; } // Preferred Audio Codec Future setPreferredAudioCodec(String codec) async { await _prefs.setString(_keyPreferredAudioCodec, codec); } String getPreferredAudioCodec() { return _prefs.getString(_keyPreferredAudioCodec) ?? 'auto'; } // Library Density Future setLibraryDensity(LibraryDensity density) async { await _prefs.setString(_keyLibraryDensity, density.name); } LibraryDensity getLibraryDensity() { final densityString = _prefs.getString(_keyLibraryDensity); return LibraryDensity.values.firstWhere( (density) => density.name == densityString, orElse: () => LibraryDensity.normal, ); } // View Mode Future setViewMode(ViewMode mode) async { await _prefs.setString(_keyViewMode, mode.name); } ViewMode getViewMode() { final modeString = _prefs.getString(_keyViewMode); return ViewMode.values.firstWhere( (mode) => mode.name == modeString, orElse: () => ViewMode.grid, ); } // Use Season Poster Future setUseSeasonPoster(bool enabled) async { await _prefs.setBool(_keyUseSeasonPoster, enabled); } bool getUseSeasonPoster() { return _prefs.getBool(_keyUseSeasonPoster) ?? false; // Default: false (use series poster) } // Show Hero Section Future setShowHeroSection(bool enabled) async { await _prefs.setBool(_keyShowHeroSection, enabled); } bool getShowHeroSection() { return _prefs.getBool(_keyShowHeroSection) ?? true; // Default: true (show hero section) } // Seek Time Small (in seconds) Future setSeekTimeSmall(int seconds) async { await _prefs.setInt(_keySeekTimeSmall, seconds); } int getSeekTimeSmall() { return _prefs.getInt(_keySeekTimeSmall) ?? 10; // Default: 10 seconds } // Seek Time Large (in seconds) Future setSeekTimeLarge(int seconds) async { await _prefs.setInt(_keySeekTimeLarge, seconds); } int getSeekTimeLarge() { return _prefs.getInt(_keySeekTimeLarge) ?? 30; // Default: 30 seconds } // Keyboard Shortcuts (Legacy String-based) Map getDefaultKeyboardShortcuts() { return { 'play_pause': 'Space', 'volume_up': 'Arrow Up', 'volume_down': 'Arrow Down', 'seek_forward': 'Arrow Right', 'seek_backward': 'Arrow Left', 'seek_forward_large': 'Shift+Arrow Right', 'seek_backward_large': 'Shift+Arrow Left', 'fullscreen_toggle': 'F', 'mute_toggle': 'M', 'subtitle_toggle': 'S', 'audio_track_next': 'A', 'subtitle_track_next': 'Shift+S', 'chapter_next': 'N', 'chapter_previous': 'P', 'speed_increase': 'Plus', 'speed_decrease': 'Minus', 'speed_reset': 'R', }; } // HotKey Objects (New implementation) Map getDefaultKeyboardHotkeys() { return { 'play_pause': HotKey(key: PhysicalKeyboardKey.space), 'volume_up': HotKey(key: PhysicalKeyboardKey.arrowUp), 'volume_down': HotKey(key: PhysicalKeyboardKey.arrowDown), 'seek_forward': HotKey(key: PhysicalKeyboardKey.arrowRight), 'seek_backward': HotKey(key: PhysicalKeyboardKey.arrowLeft), 'seek_forward_large': HotKey( key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift], ), 'seek_backward_large': HotKey( key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift], ), 'fullscreen_toggle': HotKey(key: PhysicalKeyboardKey.keyF), 'mute_toggle': HotKey(key: PhysicalKeyboardKey.keyM), 'subtitle_toggle': HotKey(key: PhysicalKeyboardKey.keyS), 'audio_track_next': HotKey(key: PhysicalKeyboardKey.keyA), 'subtitle_track_next': HotKey( key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift], ), 'chapter_next': HotKey(key: PhysicalKeyboardKey.keyN), 'chapter_previous': HotKey(key: PhysicalKeyboardKey.keyP), 'speed_increase': HotKey(key: PhysicalKeyboardKey.equal), 'speed_decrease': HotKey(key: PhysicalKeyboardKey.minus), 'speed_reset': HotKey(key: PhysicalKeyboardKey.keyR), }; } Future setKeyboardShortcuts(Map shortcuts) async { final jsonString = json.encode(shortcuts); await _prefs.setString(_keyKeyboardShortcuts, jsonString); } Map getKeyboardShortcuts() { final jsonString = _prefs.getString(_keyKeyboardShortcuts); if (jsonString == null) return getDefaultKeyboardShortcuts(); try { final decoded = json.decode(jsonString) as Map; final shortcuts = decoded.map( (key, value) => MapEntry(key, value.toString()), ); // Merge with defaults to ensure all keys exist final defaults = getDefaultKeyboardShortcuts(); defaults.addAll(shortcuts); return defaults; } catch (e) { return getDefaultKeyboardShortcuts(); } } Future setKeyboardShortcut(String action, String key) async { final shortcuts = getKeyboardShortcuts(); shortcuts[action] = key; await setKeyboardShortcuts(shortcuts); } String getKeyboardShortcut(String action) { final shortcuts = getKeyboardShortcuts(); return shortcuts[action] ?? ''; } Future resetKeyboardShortcuts() async { await setKeyboardShortcuts(getDefaultKeyboardShortcuts()); } // HotKey Objects Methods Future setKeyboardHotkeys(Map hotkeys) async { final Map> serializedHotkeys = {}; for (final entry in hotkeys.entries) { serializedHotkeys[entry.key] = _serializeHotKey(entry.value); } final jsonString = json.encode(serializedHotkeys); await _prefs.setString(_keyKeyboardHotkeys, jsonString); } Future> getKeyboardHotkeys() async { final jsonString = _prefs.getString(_keyKeyboardHotkeys); if (jsonString == null) { return getDefaultKeyboardHotkeys(); } try { final decoded = json.decode(jsonString) as Map; final Map hotkeys = {}; for (final entry in decoded.entries) { final hotKey = _deserializeHotKey(entry.value as Map); if (hotKey != null) { hotkeys[entry.key] = hotKey; } } // Merge with defaults to ensure all keys exist, but keep saved hotkeys priority final defaults = getDefaultKeyboardHotkeys(); final result = {}; // Start with defaults result.addAll(defaults); // Override with saved hotkeys (this preserves user customizations) result.addAll(hotkeys); return result; } catch (e) { return getDefaultKeyboardHotkeys(); } } Future setKeyboardHotkey(String action, HotKey hotKey) async { final hotkeys = await getKeyboardHotkeys(); hotkeys[action] = hotKey; await setKeyboardHotkeys(hotkeys); } Future getKeyboardHotkey(String action) async { final hotkeys = await getKeyboardHotkeys(); return hotkeys[action]; } Future resetKeyboardHotkeys() async { await setKeyboardHotkeys(getDefaultKeyboardHotkeys()); } // Helper methods for HotKey serialization Map _serializeHotKey(HotKey hotKey) { return { 'key': hotKey.key.toString(), 'modifiers': hotKey.modifiers?.map((m) => m.name).toList() ?? [], }; } HotKey? _deserializeHotKey(Map data) { try { final keyString = data['key'] as String; final modifierNames = (data['modifiers'] as List).cast(); final modifiers = modifierNames .map((name) { switch (name) { case 'alt': return HotKeyModifier.alt; case 'control': return HotKeyModifier.control; case 'shift': return HotKeyModifier.shift; case 'meta': return HotKeyModifier.meta; case 'capsLock': return HotKeyModifier.capsLock; case 'fn': return HotKeyModifier.fn; default: return null; } }) .where((m) => m != null) .cast() .toList(); final key = _findKeyByString(keyString); if (key != null) { return HotKey( key: key, modifiers: modifiers.isNotEmpty ? modifiers : null, ); } } catch (e) { // Ignore deserialization errors } return null; } // Helper method to find PhysicalKeyboardKey by string representation PhysicalKeyboardKey? _findKeyByString(String keyString) { // Handle exact string matches first for better performance const keyMap = { 'PhysicalKeyboardKey#0002c': PhysicalKeyboardKey.space, 'PhysicalKeyboardKey#7002a': PhysicalKeyboardKey.backspace, 'PhysicalKeyboardKey#7004c': PhysicalKeyboardKey.delete, 'PhysicalKeyboardKey#70028': PhysicalKeyboardKey.enter, 'PhysicalKeyboardKey#70029': PhysicalKeyboardKey.escape, 'PhysicalKeyboardKey#7002b': PhysicalKeyboardKey.tab, 'PhysicalKeyboardKey#7004a': PhysicalKeyboardKey.home, 'PhysicalKeyboardKey#7004d': PhysicalKeyboardKey.end, 'PhysicalKeyboardKey#7004b': PhysicalKeyboardKey.pageUp, 'PhysicalKeyboardKey#7004e': PhysicalKeyboardKey.pageDown, 'PhysicalKeyboardKey#70050': PhysicalKeyboardKey.arrowLeft, 'PhysicalKeyboardKey#70052': PhysicalKeyboardKey.arrowUp, 'PhysicalKeyboardKey#7004f': PhysicalKeyboardKey.arrowRight, 'PhysicalKeyboardKey#70051': PhysicalKeyboardKey.arrowDown, }; // Check exact matches first if (keyMap.containsKey(keyString)) { return keyMap[keyString]; } // Alternative approach: extract USB HID usage code from the toString() output // Format: PhysicalKeyboardKey#ec9ed(usbHidUsage: "0x0007002c", debugName: "Space") try { final usbHidMatch = RegExp( r'usbHidUsage: "0x([0-9a-fA-F]+)"', ).firstMatch(keyString); if (usbHidMatch != null) { final usbHidCode = usbHidMatch.group(1)!.toLowerCase(); // Map USB HID codes to PhysicalKeyboardKey objects const usbHidMap = { '0007002c': PhysicalKeyboardKey.space, '0007002a': PhysicalKeyboardKey.backspace, '0007004c': PhysicalKeyboardKey.delete, '00070028': PhysicalKeyboardKey.enter, '00070029': PhysicalKeyboardKey.escape, '0007002b': PhysicalKeyboardKey.tab, '00070039': PhysicalKeyboardKey.capsLock, // Function keys '0007003a': PhysicalKeyboardKey.f1, '0007003b': PhysicalKeyboardKey.f2, '0007003c': PhysicalKeyboardKey.f3, '0007003d': PhysicalKeyboardKey.f4, '0007003e': PhysicalKeyboardKey.f5, '0007003f': PhysicalKeyboardKey.f6, '00070040': PhysicalKeyboardKey.f7, '00070041': PhysicalKeyboardKey.f8, '00070042': PhysicalKeyboardKey.f9, '00070043': PhysicalKeyboardKey.f10, '00070044': PhysicalKeyboardKey.f11, '00070045': PhysicalKeyboardKey.f12, // Number keys '00070027': PhysicalKeyboardKey.digit0, '0007001e': PhysicalKeyboardKey.digit1, '0007001f': PhysicalKeyboardKey.digit2, '00070020': PhysicalKeyboardKey.digit3, '00070021': PhysicalKeyboardKey.digit4, '00070022': PhysicalKeyboardKey.digit5, '00070023': PhysicalKeyboardKey.digit6, '00070024': PhysicalKeyboardKey.digit7, '00070025': PhysicalKeyboardKey.digit8, '00070026': PhysicalKeyboardKey.digit9, // Letter keys '00070004': PhysicalKeyboardKey.keyA, '00070005': PhysicalKeyboardKey.keyB, '00070006': PhysicalKeyboardKey.keyC, '00070007': PhysicalKeyboardKey.keyD, '00070008': PhysicalKeyboardKey.keyE, '00070009': PhysicalKeyboardKey.keyF, '0007000a': PhysicalKeyboardKey.keyG, '0007000b': PhysicalKeyboardKey.keyH, '0007000c': PhysicalKeyboardKey.keyI, '0007000d': PhysicalKeyboardKey.keyJ, '0007000e': PhysicalKeyboardKey.keyK, '0007000f': PhysicalKeyboardKey.keyL, '00070010': PhysicalKeyboardKey.keyM, '00070011': PhysicalKeyboardKey.keyN, '00070012': PhysicalKeyboardKey.keyO, '00070013': PhysicalKeyboardKey.keyP, '00070014': PhysicalKeyboardKey.keyQ, '00070015': PhysicalKeyboardKey.keyR, '00070016': PhysicalKeyboardKey.keyS, '00070017': PhysicalKeyboardKey.keyT, '00070018': PhysicalKeyboardKey.keyU, '00070019': PhysicalKeyboardKey.keyV, '0007001a': PhysicalKeyboardKey.keyW, '0007001b': PhysicalKeyboardKey.keyX, '0007001c': PhysicalKeyboardKey.keyY, '0007001d': PhysicalKeyboardKey.keyZ, // Arrow keys '00070050': PhysicalKeyboardKey.arrowLeft, '00070052': PhysicalKeyboardKey.arrowUp, '0007004f': PhysicalKeyboardKey.arrowRight, '00070051': PhysicalKeyboardKey.arrowDown, // Other common keys '0007002d': PhysicalKeyboardKey.equal, '0007002e': PhysicalKeyboardKey.minus, '0007004a': PhysicalKeyboardKey.home, '0007004d': PhysicalKeyboardKey.end, '0007004b': PhysicalKeyboardKey.pageUp, '0007004e': PhysicalKeyboardKey.pageDown, }; if (usbHidMap.containsKey(usbHidCode)) { return usbHidMap[usbHidCode]; } } } catch (e) { // Ignore parsing errors } // Fall back to contains() checks for partial matches if (keyString.contains('space')) { return PhysicalKeyboardKey.space; } else if (keyString.contains('arrowUp')) { return PhysicalKeyboardKey.arrowUp; } else if (keyString.contains('arrowDown')) { return PhysicalKeyboardKey.arrowDown; } else if (keyString.contains('arrowLeft')) { return PhysicalKeyboardKey.arrowLeft; } else if (keyString.contains('arrowRight')) { return PhysicalKeyboardKey.arrowRight; } else if (keyString.contains('equal')) { return PhysicalKeyboardKey.equal; } else if (keyString.contains('minus')) { return PhysicalKeyboardKey.minus; } else if (keyString.contains('escape')) { return PhysicalKeyboardKey.escape; } else if (keyString.contains('enter')) { return PhysicalKeyboardKey.enter; } else if (keyString.contains('tab')) { return PhysicalKeyboardKey.tab; } else if (keyString.contains('backspace')) { return PhysicalKeyboardKey.backspace; } else if (keyString.contains('delete')) { return PhysicalKeyboardKey.delete; } else if (keyString.contains('home')) { return PhysicalKeyboardKey.home; } else if (keyString.contains('end')) { return PhysicalKeyboardKey.end; } else if (keyString.contains('pageUp')) { return PhysicalKeyboardKey.pageUp; } else if (keyString.contains('pageDown')) { return PhysicalKeyboardKey.pageDown; } else { // Try function keys F1-F12 for (int i = 1; i <= 12; i++) { if (keyString.contains('f$i') || keyString.contains('F$i')) { switch (i) { case 1: return PhysicalKeyboardKey.f1; case 2: return PhysicalKeyboardKey.f2; case 3: return PhysicalKeyboardKey.f3; case 4: return PhysicalKeyboardKey.f4; case 5: return PhysicalKeyboardKey.f5; case 6: return PhysicalKeyboardKey.f6; case 7: return PhysicalKeyboardKey.f7; case 8: return PhysicalKeyboardKey.f8; case 9: return PhysicalKeyboardKey.f9; case 10: return PhysicalKeyboardKey.f10; case 11: return PhysicalKeyboardKey.f11; case 12: return PhysicalKeyboardKey.f12; } } } // Try number keys 0-9 for (int i = 0; i <= 9; i++) { if (keyString.contains('digit$i') || keyString.contains('Digit$i')) { switch (i) { case 0: return PhysicalKeyboardKey.digit0; case 1: return PhysicalKeyboardKey.digit1; case 2: return PhysicalKeyboardKey.digit2; case 3: return PhysicalKeyboardKey.digit3; case 4: return PhysicalKeyboardKey.digit4; case 5: return PhysicalKeyboardKey.digit5; case 6: return PhysicalKeyboardKey.digit6; case 7: return PhysicalKeyboardKey.digit7; case 8: return PhysicalKeyboardKey.digit8; case 9: return PhysicalKeyboardKey.digit9; } } } // Try letter keys A-Z (both upper and lower case patterns) const letterKeys = { 'A': PhysicalKeyboardKey.keyA, 'B': PhysicalKeyboardKey.keyB, 'C': PhysicalKeyboardKey.keyC, 'D': PhysicalKeyboardKey.keyD, 'E': PhysicalKeyboardKey.keyE, 'F': PhysicalKeyboardKey.keyF, 'G': PhysicalKeyboardKey.keyG, 'H': PhysicalKeyboardKey.keyH, 'I': PhysicalKeyboardKey.keyI, 'J': PhysicalKeyboardKey.keyJ, 'K': PhysicalKeyboardKey.keyK, 'L': PhysicalKeyboardKey.keyL, 'M': PhysicalKeyboardKey.keyM, 'N': PhysicalKeyboardKey.keyN, 'O': PhysicalKeyboardKey.keyO, 'P': PhysicalKeyboardKey.keyP, 'Q': PhysicalKeyboardKey.keyQ, 'R': PhysicalKeyboardKey.keyR, 'S': PhysicalKeyboardKey.keyS, 'T': PhysicalKeyboardKey.keyT, 'U': PhysicalKeyboardKey.keyU, 'V': PhysicalKeyboardKey.keyV, 'W': PhysicalKeyboardKey.keyW, 'X': PhysicalKeyboardKey.keyX, 'Y': PhysicalKeyboardKey.keyY, 'Z': PhysicalKeyboardKey.keyZ, }; for (final entry in letterKeys.entries) { if (keyString.contains('key${entry.key}') || keyString.contains('Key${entry.key}')) { return entry.value; } } return null; } } // Media Version Preferences /// Save media version preference for a series /// [seriesRatingKey] is the grandparentRatingKey for TV series, or ratingKey for movies /// [mediaIndex] is the index of the selected media version Future setMediaVersionPreference( String seriesRatingKey, int mediaIndex, ) async { final preferences = _getMediaVersionPreferences(); preferences[seriesRatingKey] = mediaIndex; final jsonString = json.encode(preferences); await _prefs.setString(_keyMediaVersionPreferences, jsonString); } /// Get saved media version preference for a series /// Returns null if no preference is saved int? getMediaVersionPreference(String seriesRatingKey) { final preferences = _getMediaVersionPreferences(); return preferences[seriesRatingKey]; } /// Clear media version preference for a series Future clearMediaVersionPreference(String seriesRatingKey) async { final preferences = _getMediaVersionPreferences(); preferences.remove(seriesRatingKey); final jsonString = json.encode(preferences); await _prefs.setString(_keyMediaVersionPreferences, jsonString); } /// Get all media version preferences Map _getMediaVersionPreferences() { final jsonString = _prefs.getString(_keyMediaVersionPreferences); if (jsonString == null) return {}; try { final decoded = json.decode(jsonString) as Map; return decoded.map((key, value) => MapEntry(key, value as int)); } catch (e) { return {}; } } // Reset all settings to defaults Future resetAllSettings() async { await Future.wait([ _prefs.remove(_keyThemeMode), _prefs.remove(_keyEnableDebugLogging), _prefs.remove(_keyBufferSize), _prefs.remove(_keyKeyboardShortcuts), _prefs.remove(_keyKeyboardHotkeys), _prefs.remove(_keyEnableHardwareDecoding), _prefs.remove(_keyPreferredVideoCodec), _prefs.remove(_keyPreferredAudioCodec), _prefs.remove(_keyLibraryDensity), _prefs.remove(_keyViewMode), _prefs.remove(_keyUseSeasonPoster), _prefs.remove(_keyShowHeroSection), _prefs.remove(_keySeekTimeSmall), _prefs.remove(_keySeekTimeLarge), _prefs.remove(_keyMediaVersionPreferences), ]); } // Clear cache (for storage cleanup) Future clearCache() async { // This would be expanded to clear various cache directories // For now, we'll just clear any cache-related preferences await Future.wait([ // Add cache clearing logic here ]); } // Get all settings as a map for debugging/export Future> getAllSettings() async { final hotkeys = await getKeyboardHotkeys(); return { 'themeMode': getThemeMode().name, 'enableDebugLogging': getEnableDebugLogging(), 'bufferSize': getBufferSize(), 'enableHardwareDecoding': getEnableHardwareDecoding(), 'preferredVideoCodec': getPreferredVideoCodec(), 'preferredAudioCodec': getPreferredAudioCodec(), 'libraryDensity': getLibraryDensity().name, 'viewMode': getViewMode().name, 'useSeasonPoster': getUseSeasonPoster(), 'seekTimeSmall': getSeekTimeSmall(), 'seekTimeLarge': getSeekTimeLarge(), 'keyboardShortcuts': getKeyboardShortcuts(), 'keyboardHotkeys': hotkeys.map( (key, value) => MapEntry(key, _serializeHotKey(value)), ), }; } }