import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; /// Mobile-specific settings (stored locally in Hive) /// Simplified version without server API dependencies class MobileSettings { final List liveTvKeywords; final List moviesKeywords; final List seriesKeywords; final bool showClock; final bool showDebugStats; final String decoderMode; // 'auto', 'mediacodec' (HW), 'no' (SW) final String playerEngine; // 'ultra' (MPV), 'lite' (ExoPlayer) final String aspectRatioMode; // 'contain', 'cover', 'fill' final bool deinterlace; final int bufferDuration; // seconds, 0 = auto final List deinterlacedChannels; // IDs of channels with forced deinterlacing /// Custom XMLTV guide URL, tried after the provider's own `xmltv.php`. final String xmltvUrl; /// Allow the built-in community XMLTV guides as the last EPG resort. final bool useCommunityEpg; const MobileSettings({ this.liveTvKeywords = const [], this.moviesKeywords = const [], this.seriesKeywords = const [], this.showClock = false, this.showDebugStats = false, this.decoderMode = 'auto', this.playerEngine = 'ultra', this.aspectRatioMode = 'contain', this.deinterlace = false, this.bufferDuration = 0, this.deinterlacedChannels = const [], this.xmltvUrl = '', this.useCommunityEpg = true, }); MobileSettings copyWith({ List? liveTvKeywords, List? moviesKeywords, List? seriesKeywords, bool? showClock, bool? showDebugStats, String? decoderMode, String? playerEngine, String? aspectRatioMode, bool? deinterlace, int? bufferDuration, List? deinterlacedChannels, String? xmltvUrl, bool? useCommunityEpg, }) { return MobileSettings( liveTvKeywords: liveTvKeywords ?? this.liveTvKeywords, moviesKeywords: moviesKeywords ?? this.moviesKeywords, seriesKeywords: seriesKeywords ?? this.seriesKeywords, showClock: showClock ?? this.showClock, showDebugStats: showDebugStats ?? this.showDebugStats, decoderMode: decoderMode ?? this.decoderMode, playerEngine: playerEngine ?? this.playerEngine, aspectRatioMode: aspectRatioMode ?? this.aspectRatioMode, deinterlace: deinterlace ?? this.deinterlace, bufferDuration: bufferDuration ?? this.bufferDuration, deinterlacedChannels: deinterlacedChannels ?? this.deinterlacedChannels, xmltvUrl: xmltvUrl ?? this.xmltvUrl, useCommunityEpg: useCommunityEpg ?? this.useCommunityEpg, ); } /// Check if category matches Live TV filter bool matchesLiveTvFilter(String? category) { if (liveTvKeywords.isEmpty) return true; if (category == null) return true; final lowerCategory = category.toLowerCase(); return liveTvKeywords.any( (keyword) => lowerCategory.contains(keyword.toLowerCase()), ); } /// Check if category matches Movies filter bool matchesMoviesFilter(String? category) { if (moviesKeywords.isEmpty) return true; if (category == null) return true; final lowerCategory = category.toLowerCase(); return moviesKeywords.any( (keyword) => lowerCategory.contains(keyword.toLowerCase()), ); } /// Check if category matches Series filter bool matchesSeriesFilter(String? category) { if (seriesKeywords.isEmpty) return true; if (category == null) return true; final lowerCategory = category.toLowerCase(); return seriesKeywords.any( (keyword) => lowerCategory.contains(keyword.toLowerCase()), ); } } class MobileSettingsNotifier extends StateNotifier { static const String _boxName = 'mobile_settings'; Box? _box; MobileSettingsNotifier() : super(const MobileSettings()) { _init(); } Future _init() async { _box = await Hive.openBox(_boxName); final liveTv = _box?.get('liveTvKeywords', defaultValue: []) as List?; final movies = _box?.get('moviesKeywords', defaultValue: []) as List?; final series = _box?.get('seriesKeywords', defaultValue: []) as List?; final showClock = _box?.get('showClock', defaultValue: false) as bool?; final showDebugStats = _box?.get('showDebugStats', defaultValue: false) as bool?; final decoderMode = _box?.get('decoderMode', defaultValue: 'auto') as String?; final playerEngine = _box?.get('playerEngine', defaultValue: 'ultra') as String?; final aspectRatioMode = _box?.get('aspectRatioMode', defaultValue: 'contain') as String?; final deinterlace = _box?.get('deinterlace', defaultValue: false) as bool?; final buffer = _box?.get('bufferDuration', defaultValue: 0) as int?; final deinterlacedChannels = _box?.get('deinterlacedChannels', defaultValue: []) as List?; final xmltvUrl = _box?.get('xmltvUrl', defaultValue: '') as String?; final useCommunityEpg = _box?.get('useCommunityEpg', defaultValue: true) as bool?; state = MobileSettings( liveTvKeywords: liveTv?.cast() ?? [], moviesKeywords: movies?.cast() ?? [], seriesKeywords: series?.cast() ?? [], showClock: showClock ?? false, showDebugStats: showDebugStats ?? false, decoderMode: decoderMode ?? 'auto', playerEngine: playerEngine ?? 'ultra', aspectRatioMode: aspectRatioMode ?? 'contain', deinterlace: deinterlace ?? false, bufferDuration: buffer ?? 0, deinterlacedChannels: deinterlacedChannels?.cast() ?? [], xmltvUrl: xmltvUrl ?? '', useCommunityEpg: useCommunityEpg ?? true, ); } void setLiveTvKeywords(List keywords) { state = state.copyWith(liveTvKeywords: keywords); _box?.put('liveTvKeywords', keywords); } void setMoviesKeywords(List keywords) { state = state.copyWith(moviesKeywords: keywords); _box?.put('moviesKeywords', keywords); } void setSeriesKeywords(List keywords) { state = state.copyWith(seriesKeywords: keywords); _box?.put('seriesKeywords', keywords); } void toggleShowClock(bool value) { state = state.copyWith(showClock: value); _box?.put('showClock', value); } void toggleShowDebugStats(bool value) { state = state.copyWith(showDebugStats: value); _box?.put('showDebugStats', value); } void setDecoderMode(String mode) { state = state.copyWith(decoderMode: mode); _box?.put('decoderMode', mode); } void setPlayerEngine(String engine) { state = state.copyWith(playerEngine: engine); _box?.put('playerEngine', engine); } void setAspectRatioMode(String mode) { state = state.copyWith(aspectRatioMode: mode); _box?.put('aspectRatioMode', mode); } void toggleDeinterlace(bool value) { state = state.copyWith(deinterlace: value); _box?.put('deinterlace', value); } void setBufferDuration(int seconds) { state = state.copyWith(bufferDuration: seconds); _box?.put('bufferDuration', seconds); } void setXmltvUrl(String url) { final trimmed = url.trim(); state = state.copyWith(xmltvUrl: trimmed); _box?.put('xmltvUrl', trimmed); } void toggleCommunityEpg(bool value) { state = state.copyWith(useCommunityEpg: value); _box?.put('useCommunityEpg', value); } void toggleChannelDeinterlace(String streamId) { final currentList = List.from(state.deinterlacedChannels); if (currentList.contains(streamId)) { currentList.remove(streamId); } else { currentList.add(streamId); } state = state.copyWith(deinterlacedChannels: currentList); _box?.put('deinterlacedChannels', currentList); } } final mobileSettingsProvider = StateNotifierProvider((ref) { return MobileSettingsNotifier(); }); /// Watch history for mobile (stored locally) class MobileWatchHistory { final Set watchedMovies; final Set watchedEpisodes; final Map resumePositions; // streamId -> position in seconds const MobileWatchHistory({ this.watchedMovies = const {}, this.watchedEpisodes = const {}, this.resumePositions = const {}, }); MobileWatchHistory copyWith({ Set? watchedMovies, Set? watchedEpisodes, Map? resumePositions, }) { return MobileWatchHistory( watchedMovies: watchedMovies ?? this.watchedMovies, watchedEpisodes: watchedEpisodes ?? this.watchedEpisodes, resumePositions: resumePositions ?? this.resumePositions, ); } bool isMovieWatched(String streamId) => watchedMovies.contains(streamId); bool isEpisodeWatched(String episodeKey) => watchedEpisodes.contains(episodeKey); /// Get resume position in seconds (0 if none saved) int getResumePosition(String contentId) => resumePositions[contentId] ?? 0; static String episodeKey(dynamic seriesId, int season, int episodeNum) { return '$seriesId:$season:$episodeNum'; } } class MobileWatchHistoryNotifier extends StateNotifier { static const String _boxName = 'mobile_watch_history'; Box? _box; MobileWatchHistoryNotifier() : super(const MobileWatchHistory()) { _init(); } Future _init() async { _box = await Hive.openBox(_boxName); final movies = _box?.get('watchedMovies', defaultValue: []) as List?; final episodes = _box?.get('watchedEpisodes', defaultValue: []) as List?; final positionsRaw = _box?.get('resumePositions'); // Safe conversion for resumePositions map Map positions = {}; if (positionsRaw != null && positionsRaw is Map) { positionsRaw.forEach((key, value) { if (key is String && value is int) { positions[key] = value; } else if (key is String && value is num) { positions[key] = value.toInt(); } }); } state = MobileWatchHistory( watchedMovies: movies?.cast().toSet() ?? {}, watchedEpisodes: episodes?.cast().toSet() ?? {}, resumePositions: positions, ); } void markMovieWatched(String streamId) { final updated = {...state.watchedMovies, streamId}; // Clear resume position when marked as fully watched final positions = {...state.resumePositions}; positions.remove(streamId); state = state.copyWith(watchedMovies: updated, resumePositions: positions); _box?.put('watchedMovies', updated.toList()); _box?.put('resumePositions', positions); } void toggleMovieWatched(String streamId) { final movies = {...state.watchedMovies}; if (movies.contains(streamId)) { movies.remove(streamId); } else { movies.add(streamId); } state = state.copyWith(watchedMovies: movies); _box?.put('watchedMovies', movies.toList()); } void markEpisodeWatched(String episodeKey) { final updated = {...state.watchedEpisodes, episodeKey}; // Clear resume position when marked as fully watched final positions = {...state.resumePositions}; positions.remove(episodeKey); state = state.copyWith(watchedEpisodes: updated, resumePositions: positions); _box?.put('watchedEpisodes', updated.toList()); _box?.put('resumePositions', positions); } /// Save resume position for movie or episode void saveResumePosition(String contentId, int positionSeconds) { // Only save if position is meaningful (> 30 seconds) if (positionSeconds < 30) { debugPrint( '[WatchHistory] Position too short ($positionSeconds), skipping save', ); return; } debugPrint( '[WatchHistory] Saving position for $contentId: ${positionSeconds}s', ); final positions = {...state.resumePositions, contentId: positionSeconds}; state = state.copyWith(resumePositions: positions); _box?.put('resumePositions', Map.from(positions)); } /// Clear resume position (when content finished or user wants to restart) void clearResumePosition(String contentId) { final positions = {...state.resumePositions}; positions.remove(contentId); state = state.copyWith(resumePositions: positions); _box?.put('resumePositions', positions); } } final mobileWatchHistoryProvider = StateNotifierProvider( (ref) { return MobileWatchHistoryNotifier(); }); /// Favorites for mobile (stored locally) class MobileFavoritesNotifier extends StateNotifier> { static const String _boxName = 'mobile_favorites'; Box? _box; MobileFavoritesNotifier() : super({}) { _init(); } Future _init() async { _box = await Hive.openBox(_boxName); final favorites = _box?.get('favorites', defaultValue: []) as List?; state = favorites?.cast().toSet() ?? {}; } void toggle(String streamId) { final updated = {...state}; if (updated.contains(streamId)) { updated.remove(streamId); } else { updated.add(streamId); } state = updated; _box?.put('favorites', updated.toList()); } bool isFavorite(String streamId) => state.contains(streamId); } final mobileFavoritesProvider = StateNotifierProvider>((ref) { return MobileFavoritesNotifier(); }); /// Per-channel deinterlace settings (stored locally) class DeinterlaceSettingsNotifier extends StateNotifier> { static const String _boxName = 'deinterlace_settings'; Box? _box; DeinterlaceSettingsNotifier() : super({}) { _init(); } Future _init() async { _box = await Hive.openBox(_boxName); final settings = _box?.get('deinterlace_on', defaultValue: []) as List?; state = settings?.cast().toSet() ?? {}; } void toggle(String streamId) { final updated = {...state}; if (updated.contains(streamId)) { updated.remove(streamId); } else { updated.add(streamId); } state = updated; _box?.put('deinterlace_on', updated.toList()); } bool isEnabled(String streamId) => state.contains(streamId); } final deinterlaceSettingsProvider = StateNotifierProvider>((ref) { return DeinterlaceSettingsNotifier(); });