feat: introduce IPTV settings provider with configurable streaming, EPG, and player preferences

This commit is contained in:
Michael committed 2025-12-08 22:33:54 +01:00
1 parent c484ed9994
commit 80d93469f8
1 file changed
+41 -146
@@ -1,5 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/services/settings_api_service.dart'; import '../../../core/services/settings_api_service.dart';
import '../../auth/providers/auth_provider.dart'; import '../../auth/providers/auth_provider.dart';
@@ -38,7 +37,7 @@ enum TranscodingMode {
disabled, // Direct stream (no transcoding) disabled, // Direct stream (no transcoding)
} }
/// Keys for SharedPreferences storage /// Keys for API JSON storage
class _SettingsKeys { class _SettingsKeys {
// Filters // Filters
static const String liveTvFilter = 'filter_live_tv'; static const String liveTvFilter = 'filter_live_tv';
@@ -227,93 +226,44 @@ class IptvSettings {
bool matchesFilter(String categoryName) => matchesLiveTvFilter(categoryName); bool matchesFilter(String categoryName) => matchesLiveTvFilter(categoryName);
} }
/// IPTV Settings notifier with SharedPreferences and API persistence /// Settings notifier with API persistence only (No Local Cache)
class IptvSettingsNotifier extends StateNotifier<IptvSettings> { class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
SharedPreferences? _prefs;
final SettingsApiService _apiService = SettingsApiService(); final SettingsApiService _apiService = SettingsApiService();
final Ref _ref; final Ref _ref;
// ignore: unused_field
bool _initialized = false; bool _initialized = false;
IptvSettingsNotifier(this._ref) : super(const IptvSettings()) { IptvSettingsNotifier(this._ref) : super(const IptvSettings()) {
_loadSettings(); _init();
_setupAuthListener(); _setupAuthListener();
} }
void _init() {
final authState = _ref.read(authProvider);
if (authState.isAuthenticated) {
_fetchFromApi();
}
}
void _setupAuthListener() { void _setupAuthListener() {
_ref.listen<AuthState>(authProvider, (previous, next) { _ref.listen<AuthState>(authProvider, (previous, next) {
// Sync on login
if (previous?.isAuthenticated == false && next.isAuthenticated) { if (previous?.isAuthenticated == false && next.isAuthenticated) {
// User just logged in, sync settings from API
_fetchFromApi(); _fetchFromApi();
} }
}); // Reset on logout
} if (previous?.isAuthenticated == true && !next.isAuthenticated) {
state = const IptvSettings();
/// Load settings from SharedPreferences and then API _initialized = false;
Future<void> _loadSettings() async {
if (_initialized) return;
try {
_prefs = await SharedPreferences.getInstance();
// Load local settings first (fastest)
final liveTv = _prefs?.getString(_SettingsKeys.liveTvFilter) ?? '';
final movies = _prefs?.getString(_SettingsKeys.moviesFilter) ?? '';
final series = _prefs?.getString(_SettingsKeys.seriesFilter) ?? '';
final quality = StreamQuality.values[
_prefs?.getInt(_SettingsKeys.streamQuality) ?? StreamQuality.medium.index
];
final buffer = BufferSize.values[
_prefs?.getInt(_SettingsKeys.bufferSize) ?? BufferSize.medium.index
];
final timeout = ConnectionTimeout.values[
_prefs?.getInt(_SettingsKeys.connectionTimeout) ?? ConnectionTimeout.medium.index
];
final reconnect = _prefs?.getBool(_SettingsKeys.autoReconnect) ?? true;
final epgCache = EpgCacheDuration.values[
_prefs?.getInt(_SettingsKeys.epgCacheDuration) ?? EpgCacheDuration.medium.index
];
final transcoding = TranscodingMode.values[
_prefs?.getInt(_SettingsKeys.transcodingMode) ?? TranscodingMode.auto.index
];
final directPlay = _prefs?.getBool(_SettingsKeys.preferDirectPlay) ?? false;
final showClock = _prefs?.getBool(_SettingsKeys.showClock) ?? false;
final aspectRatio = _prefs?.getString(_SettingsKeys.preferredAspectRatio) ?? 'contain';
state = IptvSettings(
liveTvCategoryFilter: liveTv,
moviesCategoryFilter: movies,
seriesCategoryFilter: series,
streamQuality: quality,
bufferSize: buffer,
connectionTimeout: timeout,
autoReconnect: reconnect,
epgCacheDuration: epgCache,
transcodingMode: transcoding,
preferDirectPlay: directPlay,
showClock: showClock,
preferredAspectRatio: aspectRatio,
);
_initialized = true;
// Sync from API if logged in
final authState = _ref.read(authProvider);
if (authState.isAuthenticated) {
await _fetchFromApi();
} }
});
} catch (e) {
print('Error loading settings: $e');
}
} }
Future<void> _fetchFromApi() async { Future<void> _fetchFromApi() async {
try { try {
final remoteSettings = await _apiService.getSettings(); final remoteSettings = await _apiService.getSettings();
if (remoteSettings != null && remoteSettings.isNotEmpty) { if (remoteSettings != null && remoteSettings.isNotEmpty) {
// Update state with remote settings if they exist // Helper to safely cast dynamic types from JSON
T? getValue<T>(String key) => remoteSettings[key] as T?; T? getValue<T>(String key) => remoteSettings[key] as T?;
state = state.copyWith( state = state.copyWith(
@@ -336,10 +286,8 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
showClock: getValue<bool>(_SettingsKeys.showClock), showClock: getValue<bool>(_SettingsKeys.showClock),
preferredAspectRatio: getValue<String>(_SettingsKeys.preferredAspectRatio), preferredAspectRatio: getValue<String>(_SettingsKeys.preferredAspectRatio),
); );
// Save to local prefs to keep in sync
_saveToPrefs();
} }
_initialized = true;
} catch (e) { } catch (e) {
print('Error syncing settings from API: $e'); print('Error syncing settings from API: $e');
} }
@@ -368,72 +316,21 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
await _apiService.saveSettings(settingsMap); await _apiService.saveSettings(settingsMap);
} }
// ===== Save Helpers ===== // ===== Setters with Auto-Save =====
Future<void> _saveString(String key, String value) async {
try {
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setString(key, value);
await _saveToApi();
} catch (e) {
print('Error saving setting $key: $e');
}
}
Future<void> _saveInt(String key, int value) async {
try {
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setInt(key, value);
await _saveToApi();
} catch (e) {
print('Error saving setting $key: $e');
}
}
Future<void> _saveBool(String key, bool value) async {
try {
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setBool(key, value);
await _saveToApi();
} catch (e) {
print('Error saving setting $key: $e');
}
}
Future<void> _saveToPrefs() async {
// Helper to bulk save state to prefs (after API fetch)
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setString(_SettingsKeys.liveTvFilter, state.liveTvCategoryFilter);
await _prefs?.setString(_SettingsKeys.moviesFilter, state.moviesCategoryFilter);
await _prefs?.setString(_SettingsKeys.seriesFilter, state.seriesCategoryFilter);
await _prefs?.setInt(_SettingsKeys.streamQuality, state.streamQuality.index);
await _prefs?.setInt(_SettingsKeys.bufferSize, state.bufferSize.index);
await _prefs?.setInt(_SettingsKeys.connectionTimeout, state.connectionTimeout.index);
await _prefs?.setBool(_SettingsKeys.autoReconnect, state.autoReconnect);
await _prefs?.setInt(_SettingsKeys.epgCacheDuration, state.epgCacheDuration.index);
await _prefs?.setInt(_SettingsKeys.transcodingMode, state.transcodingMode.index);
await _prefs?.setBool(_SettingsKeys.preferDirectPlay, state.preferDirectPlay);
await _prefs?.setBool(_SettingsKeys.showClock, state.showClock);
await _prefs?.setString(_SettingsKeys.preferredAspectRatio, state.preferredAspectRatio);
}
// ===== Filter Setters =====
void setLiveTvFilter(String filter) { void setLiveTvFilter(String filter) {
state = state.copyWith(liveTvCategoryFilter: filter); state = state.copyWith(liveTvCategoryFilter: filter);
_saveString(_SettingsKeys.liveTvFilter, filter); _saveToApi();
} }
void setMoviesFilter(String filter) { void setMoviesFilter(String filter) {
state = state.copyWith(moviesCategoryFilter: filter); state = state.copyWith(moviesCategoryFilter: filter);
_saveString(_SettingsKeys.moviesFilter, filter); _saveToApi();
} }
void setSeriesFilter(String filter) { void setSeriesFilter(String filter) {
state = state.copyWith(seriesCategoryFilter: filter); state = state.copyWith(seriesCategoryFilter: filter);
_saveString(_SettingsKeys.seriesFilter, filter); _saveToApi();
} }
void clearLiveTvFilter() => setLiveTvFilter(''); void clearLiveTvFilter() => setLiveTvFilter('');
@@ -441,58 +338,61 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
void clearSeriesFilter() => setSeriesFilter(''); void clearSeriesFilter() => setSeriesFilter('');
void clearAllFilters() { void clearAllFilters() {
clearLiveTvFilter(); state = state.copyWith(
clearMoviesFilter(); liveTvCategoryFilter: '',
clearSeriesFilter(); moviesCategoryFilter: '',
seriesCategoryFilter: '',
);
_saveToApi();
} }
// ===== Streaming Setters ===== // ===== Streaming Setters =====
void setStreamQuality(StreamQuality quality) { void setStreamQuality(StreamQuality quality) {
state = state.copyWith(streamQuality: quality); state = state.copyWith(streamQuality: quality);
_saveInt(_SettingsKeys.streamQuality, quality.index); _saveToApi();
} }
void setBufferSize(BufferSize buffer) { void setBufferSize(BufferSize buffer) {
state = state.copyWith(bufferSize: buffer); state = state.copyWith(bufferSize: buffer);
_saveInt(_SettingsKeys.bufferSize, buffer.index); _saveToApi();
} }
void setConnectionTimeout(ConnectionTimeout timeout) { void setConnectionTimeout(ConnectionTimeout timeout) {
state = state.copyWith(connectionTimeout: timeout); state = state.copyWith(connectionTimeout: timeout);
_saveInt(_SettingsKeys.connectionTimeout, timeout.index); _saveToApi();
} }
void setAutoReconnect(bool value) { void setAutoReconnect(bool value) {
state = state.copyWith(autoReconnect: value); state = state.copyWith(autoReconnect: value);
_saveBool(_SettingsKeys.autoReconnect, value); _saveToApi();
} }
void setEpgCacheDuration(EpgCacheDuration duration) { void setEpgCacheDuration(EpgCacheDuration duration) {
state = state.copyWith(epgCacheDuration: duration); state = state.copyWith(epgCacheDuration: duration);
_saveInt(_SettingsKeys.epgCacheDuration, duration.index); _saveToApi();
} }
void setTranscodingMode(TranscodingMode mode) { void setTranscodingMode(TranscodingMode mode) {
state = state.copyWith(transcodingMode: mode); state = state.copyWith(transcodingMode: mode);
_saveInt(_SettingsKeys.transcodingMode, mode.index); _saveToApi();
} }
void setPreferDirectPlay(bool value) { void setPreferDirectPlay(bool value) {
state = state.copyWith(preferDirectPlay: value); state = state.copyWith(preferDirectPlay: value);
_saveBool(_SettingsKeys.preferDirectPlay, value); _saveToApi();
} }
// ===== Player Display Setters ===== // ===== Player Display Setters =====
void setShowClock(bool value) { void setShowClock(bool value) {
state = state.copyWith(showClock: value); state = state.copyWith(showClock: value);
_saveBool(_SettingsKeys.showClock, value); _saveToApi();
} }
void setPreferredAspectRatio(String value) { void setPreferredAspectRatio(String value) {
state = state.copyWith(preferredAspectRatio: value); state = state.copyWith(preferredAspectRatio: value);
_saveString(_SettingsKeys.preferredAspectRatio, value); _saveToApi();
} }
// Legacy methods // Legacy methods
@@ -504,12 +404,7 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
final iptvSettingsProvider = final iptvSettingsProvider =
StateNotifierProvider<IptvSettingsNotifier, IptvSettings>((ref) { StateNotifierProvider<IptvSettingsNotifier, IptvSettings>((ref) {
// Watch auth provider to re-fetch settings on login/logout // Watch auth provider to re-fetch settings on login/logout
final authState = ref.watch(authProvider); // We just watch it to ensure the provider tree updates, but the listener above handles logic
final notifier = IptvSettingsNotifier(ref); ref.watch(authProvider);
return IptvSettingsNotifier(ref);
// Note: Since we watch authProvider, this provider will be disposed and recreated
// whenever authState changes. This automatically handles fetching settings for
// the new user on creation.
return notifier;
}); });