feat: introduce IPTV settings management with a dedicated mobile tab, providers, and service.

This commit is contained in:
Michael committed 2026-03-09 13:31:01 +01:00
1 parent 7ea12511a0
commit 7eb483fc07
5 files changed
+88 -9

No files matched your search

@@ -65,6 +65,7 @@ class _SettingsKeys {
static const String playerType = 'player_type'; static const String playerType = 'player_type';
// Server/GPU Settings // Server/GPU Settings
static const String enableNvidiaGpu = 'enable_nvidia_gpu'; static const String enableNvidiaGpu = 'enable_nvidia_gpu';
static const String backendUrl = 'backend_url';
} }
/// Settings state for IPTV preferences with persistence /// Settings state for IPTV preferences with persistence
@@ -90,6 +91,7 @@ class IptvSettings {
// Server/GPU Settings // Server/GPU Settings
final bool enableNvidiaGpu; final bool enableNvidiaGpu;
final String backendUrl; // Manual override
const IptvSettings({ const IptvSettings({
// Filters // Filters
@@ -110,6 +112,7 @@ class IptvSettings {
this.playerType = PlayerType.standard, this.playerType = PlayerType.standard,
// Server/GPU defaults // Server/GPU defaults
this.enableNvidiaGpu = false, this.enableNvidiaGpu = false,
this.backendUrl = '',
}); });
IptvSettings copyWith({ IptvSettings copyWith({
@@ -127,6 +130,7 @@ class IptvSettings {
String? preferredAspectRatio, String? preferredAspectRatio,
PlayerType? playerType, PlayerType? playerType,
bool? enableNvidiaGpu, bool? enableNvidiaGpu,
String? backendUrl,
}) { }) {
return IptvSettings( return IptvSettings(
liveTvCategoryFilter: liveTvCategoryFilter ?? this.liveTvCategoryFilter, liveTvCategoryFilter: liveTvCategoryFilter ?? this.liveTvCategoryFilter,
@@ -143,6 +147,7 @@ class IptvSettings {
preferredAspectRatio: preferredAspectRatio ?? this.preferredAspectRatio, preferredAspectRatio: preferredAspectRatio ?? this.preferredAspectRatio,
playerType: playerType ?? this.playerType, playerType: playerType ?? this.playerType,
enableNvidiaGpu: enableNvidiaGpu ?? this.enableNvidiaGpu, enableNvidiaGpu: enableNvidiaGpu ?? this.enableNvidiaGpu,
backendUrl: backendUrl ?? this.backendUrl,
); );
} }
@@ -344,6 +349,7 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
.values[remoteSettings[_SettingsKeys.playerType] as int] .values[remoteSettings[_SettingsKeys.playerType] as int]
: null, : null,
enableNvidiaGpu: getValue<bool>(_SettingsKeys.enableNvidiaGpu), enableNvidiaGpu: getValue<bool>(_SettingsKeys.enableNvidiaGpu),
backendUrl: getValue<String>(_SettingsKeys.backendUrl),
); );
} }
_initialized = true; _initialized = true;
@@ -388,6 +394,7 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
_SettingsKeys.preferredAspectRatio: state.preferredAspectRatio, _SettingsKeys.preferredAspectRatio: state.preferredAspectRatio,
_SettingsKeys.playerType: state.playerType.index, _SettingsKeys.playerType: state.playerType.index,
_SettingsKeys.enableNvidiaGpu: state.enableNvidiaGpu, _SettingsKeys.enableNvidiaGpu: state.enableNvidiaGpu,
_SettingsKeys.backendUrl: state.backendUrl,
}; };
print('[SettingsProvider] _saveToApi: Saving settings...'); print('[SettingsProvider] _saveToApi: Saving settings...');
@@ -487,6 +494,11 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
_saveToApi(); _saveToApi();
} }
void setBackendUrl(String value) {
state = state.copyWith(backendUrl: value);
_saveToApi();
}
// Legacy methods // Legacy methods
void setCategoryFilter(String filter) => setLiveTvFilter(filter); void setCategoryFilter(String filter) => setLiveTvFilter(filter);
void clearCategoryFilter() => clearLiveTvFilter(); void clearCategoryFilter() => clearLiveTvFilter();
@@ -1,10 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/playlist_config.dart'; import '../../../core/models/playlist_config.dart';
import '../services/xtream_service.dart'; import '../services/xtream_service.dart';
import 'settings_provider.dart';
/// Family provider for Xtream service - one instance per playlist /// Family provider for Xtream service - one instance per playlist
final xtreamServiceProvider = Provider.family<XtreamService, PlaylistConfig>((ref, playlist) { final xtreamServiceProvider =
Provider.family<XtreamService, PlaylistConfig>((ref, playlist) {
final service = XtreamService(); final service = XtreamService();
final settings = ref.watch(iptvSettingsProvider);
service.setBackendUrl(settings.backendUrl);
service.setPlaylist(playlist); service.setPlaylist(playlist);
ref.onDispose(() => service.dispose()); ref.onDispose(() => service.dispose());
return service; return service;
@@ -13,6 +17,8 @@ final xtreamServiceProvider = Provider.family<XtreamService, PlaylistConfig>((re
/// Singleton Xtream service provider (legacy, for activeXtreamServiceProvider) /// Singleton Xtream service provider (legacy, for activeXtreamServiceProvider)
final _singletonXtreamServiceProvider = Provider<XtreamService>((ref) { final _singletonXtreamServiceProvider = Provider<XtreamService>((ref) {
final service = XtreamService(); final service = XtreamService();
final settings = ref.watch(iptvSettingsProvider);
service.setBackendUrl(settings.backendUrl);
ref.onDispose(() => service.dispose()); ref.onDispose(() => service.dispose());
return service; return service;
}); });
@@ -39,7 +45,8 @@ final liveChannelsProvider = FutureProvider.autoDispose((ref) async {
}); });
/// Provider for live TV channels by playlist (family provider) /// Provider for live TV channels by playlist (family provider)
final liveChannelsByPlaylistProvider = FutureProvider.family.autoDispose((ref, PlaylistConfig playlist) async { final liveChannelsByPlaylistProvider =
FutureProvider.family.autoDispose((ref, PlaylistConfig playlist) async {
final service = ref.watch(xtreamServiceProvider(playlist)); final service = ref.watch(xtreamServiceProvider(playlist));
return await service.getLiveChannels(); return await service.getLiveChannels();
}); });
@@ -57,7 +64,8 @@ final seriesProvider = FutureProvider.autoDispose((ref) async {
}); });
/// Provider for EPG of a specific stream /// Provider for EPG of a specific stream
final epgProvider = FutureProvider.family.autoDispose((ref, String streamId) async { final epgProvider =
FutureProvider.family.autoDispose((ref, String streamId) async {
final service = ref.watch(activeXtreamServiceProvider); final service = ref.watch(activeXtreamServiceProvider);
return await service.getShortEpg(streamId); return await service.getShortEpg(streamId);
}); });
@@ -82,7 +90,8 @@ class EpgRequestKey {
} }
/// Provider for EPG with playlist context /// Provider for EPG with playlist context
final epgByPlaylistProvider = FutureProvider.family.autoDispose((ref, EpgRequestKey key) async { final epgByPlaylistProvider =
FutureProvider.family.autoDispose((ref, EpgRequestKey key) async {
final service = ref.watch(xtreamServiceProvider(key.playlist)); final service = ref.watch(xtreamServiceProvider(key.playlist));
return await service.getShortEpg(key.streamId); return await service.getShortEpg(key.streamId);
}); });
+21 -4
View File
@@ -34,17 +34,34 @@ class XtreamService {
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions)); _dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} }
String? _manualBackendUrl;
/// Get dynamic base URL for the local backend /// Get dynamic base URL for the local backend
/// On Web: use localhost (proxy handles it) /// Priority:
/// On Mobile: default to host machine IP for emulator /// 1. Manual override from settings
/// 2. Detected origin (Web)
/// 3. Host machine IP (Emulator)
String get _backendBaseUrl { String get _backendBaseUrl {
if (kIsWeb) return 'http://localhost:8089'; if (_manualBackendUrl != null && _manualBackendUrl!.isNotEmpty) {
// On Android emulator, 10.0.2.2 points to host machine's localhost return _manualBackendUrl!;
}
if (kIsWeb) {
final origin = Uri.base.origin;
if (origin.contains('localhost') && !origin.contains('8089')) {
return 'http://localhost:8089';
}
return origin;
}
return 'http://10.0.2.2:8089'; return 'http://10.0.2.2:8089';
} }
String get backendBaseUrl => _backendBaseUrl; String get backendBaseUrl => _backendBaseUrl;
/// Set manual backend URL override
void setBackendUrl(String? url) {
_manualBackendUrl = url;
}
/// Wrap URL with proxy for all external IPTV URLs /// Wrap URL with proxy for all external IPTV URLs
String _wrapWithProxy(String url) { String _wrapWithProxy(String url) {
if (url.startsWith('http')) { if (url.startsWith('http')) {
@@ -17,7 +17,8 @@ class MobileSettingsTab extends ConsumerStatefulWidget {
class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> { class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
late TextEditingController _liveTvController; late TextEditingController _liveTvController;
late TextEditingController _moviesController; late TextEditingController _moviesController;
late TextEditingController _seriesController; late final TextEditingController _seriesController;
late final TextEditingController _backendUrlController;
bool _initialized = false; bool _initialized = false;
@override @override
@@ -26,6 +27,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
_liveTvController = TextEditingController(); _liveTvController = TextEditingController();
_moviesController = TextEditingController(); _moviesController = TextEditingController();
_seriesController = TextEditingController(); _seriesController = TextEditingController();
_backendUrlController = TextEditingController();
} }
@override @override
@@ -33,6 +35,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
_liveTvController.dispose(); _liveTvController.dispose();
_moviesController.dispose(); _moviesController.dispose();
_seriesController.dispose(); _seriesController.dispose();
_backendUrlController.dispose();
super.dispose(); super.dispose();
} }
@@ -49,6 +52,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
_seriesController.text != settings.seriesCategoryFilter) { _seriesController.text != settings.seriesCategoryFilter) {
_seriesController.text = settings.seriesCategoryFilter; _seriesController.text = settings.seriesCategoryFilter;
} }
if (!_initialized || _backendUrlController.text != settings.backendUrl) {
_backendUrlController.text = settings.backendUrl;
}
_initialized = true; _initialized = true;
} }
@@ -273,6 +279,40 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
const _SectionHeader(title: 'Server Settings'),
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(16),
child: Column(
children: [
_FilterInput(
label: 'Backend URL Override',
icon: Icons.lan,
controller: _backendUrlController,
onChanged: (val) => ref
.read(iptvSettingsProvider.notifier)
.setBackendUrl(val),
),
if (settings.backendUrl.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 8, left: 32),
child: Text(
'Currently using: ${Uri.base.origin}',
style: const TextStyle(
fontSize: 10,
color: AppColors.textSecondary,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
const SizedBox(height: 24),
// Content Filters // Content Filters
const _SectionHeader(title: 'Content Filters'), const _SectionHeader(title: 'Content Filters'),
Container( Container(
+1
View File
@@ -19,6 +19,7 @@ Refonte du Guide TV pour inclure les catégories et corriger l'EPG.
- [x] Correction de l'endpoint EPG Backend (Fallback) - [x] Correction de l'endpoint EPG Backend (Fallback)
- [x] Correction du lecteur Mobile (LitePlayerView) - [x] Correction du lecteur Mobile (LitePlayerView)
- [x] Alignement UI Mobile (Tri, EPG, URLs) - [x] Alignement UI Mobile (Tri, EPG, URLs)
- [x] Correction Connectivité Mobile (Auto-origin + Manuel Override)
## Progress Log ## Progress Log