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 int bufferDuration; // seconds, 0 = auto const MobileSettings({ this.liveTvKeywords = const [], this.moviesKeywords = const [], this.seriesKeywords = const [], this.showClock = false, this.bufferDuration = 0, }); MobileSettings copyWith({ List? liveTvKeywords, List? moviesKeywords, List? seriesKeywords, bool? showClock, int? bufferDuration, }) { return MobileSettings( liveTvKeywords: liveTvKeywords ?? this.liveTvKeywords, moviesKeywords: moviesKeywords ?? this.moviesKeywords, seriesKeywords: seriesKeywords ?? this.seriesKeywords, showClock: showClock ?? this.showClock, bufferDuration: bufferDuration ?? this.bufferDuration, ); } /// 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 buffer = _box?.get('bufferDuration', defaultValue: 0) as int?; state = MobileSettings( liveTvKeywords: liveTv?.cast() ?? [], moviesKeywords: movies?.cast() ?? [], seriesKeywords: series?.cast() ?? [], showClock: showClock ?? false, bufferDuration: buffer ?? 0, ); } 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 setBufferDuration(int seconds) { state = state.copyWith(bufferDuration: seconds); _box?.put('bufferDuration', seconds); } } 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(); });