Multi-IPTV : déclarer l'IPTV active de façon synchrone dans AuthedHttp

AuthedHttp lisait l'IPTV dans localStorage, écrit par un écouteur
Riverpod enregistré tardivement : après un changement d'IPTV l'en-tête
X-Playlist-Id pouvait rester périmé. AuthedHttp.usePlaylist pose l'IPTV
en mémoire dès la sélection et à chaque construction du tableau de bord.

Auto NBA reçoit la playlist, recharge sa règle quand elle change et
nomme l'IPTV sur laquelle les matchs sont enregistrés.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-19 18:16:35 +02:00
1 parent a6322b57b8
commit 6368fd6ee2
6 files changed
+58 -7

No files matched your search

+22 -4
View File
@@ -24,12 +24,30 @@ class AuthedHttp {
return (t != null && t.isNotEmpty) ? t : null; return (t != null && t.isNotEmpty) ? t : null;
} }
/// IPTV sélectionnée (persistée par `playlistPersistenceProvider`). /// IPTV active, posée de façon synchrone par l'écran de sélection et le
/// tableau de bord (voir [usePlaylist]).
/// ///
/// Envoyée à chaque appel : sans elle le serveur prenait la playlist la /// POURQUOI : `localStorage['selected_playlist']` n'est écrit que par un
/// plus récente, et guide, enregistrements et Auto NBA visaient la /// écouteur Riverpod enregistré tardivement ; après un changement d'IPTV
/// mauvaise IPTV quand plusieurs étaient configurées. /// il restait périmé, le serveur retombait sur la playlist par défaut et
/// les deux IPTV partageaient la même règle Auto NBA et les mêmes
/// enregistrements.
static String? _currentPlaylistId;
/// Déclare l'IPTV active et la persiste immédiatement.
static void usePlaylist(String id, {Map<String, dynamic>? json}) {
if (id.isEmpty) return;
_currentPlaylistId = id;
if (json != null) {
try {
html.window.localStorage['selected_playlist'] = jsonEncode(json);
} catch (_) {}
}
}
/// IPTV envoyée au serveur : celle déclarée, sinon la dernière persistée.
static String? get selectedPlaylistId { static String? get selectedPlaylistId {
if (_currentPlaylistId != null) return _currentPlaylistId;
try { try {
final stored = html.window.localStorage['selected_playlist']; final stored = html.window.localStorage['selected_playlist'];
if (stored == null) return null; if (stored == null) return null;
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../auth/providers/auth_provider.dart'; import '../../auth/providers/auth_provider.dart';
import '../../../core/api/authed_http.dart';
import '../../../core/models/playlist_config.dart'; import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/glass_container.dart'; import '../../../core/widgets/glass_container.dart';
@@ -43,6 +44,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Toutes les requêtes /api/* de ce tableau de bord visent cette IPTV.
AuthedHttp.usePlaylist(widget.playlist.id, json: widget.playlist.toJson());
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
body: Stack( body: Stack(
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../core/services/playlist_api_service.dart'; import '../../../core/services/playlist_api_service.dart';
import '../../../core/api/authed_http.dart';
import '../../../core/models/playlist_config.dart'; import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme.dart';
@@ -163,6 +164,10 @@ class PlaylistSelectionScreen extends ConsumerWidget {
child: _PlaylistCard( child: _PlaylistCard(
playlist: playlist, playlist: playlist,
onTap: () { onTap: () {
AuthedHttp.usePlaylist(
playlist.id,
json: playlist.toJson(),
);
selectPlaylist(ref, playlist); selectPlaylist(ref, playlist);
context.go('/dashboard'); context.go('/dashboard');
}, },
+23 -3
View File
@@ -52,6 +52,8 @@ class _RecordingsTabState extends State<RecordingsTab>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Guide, enregistrements, season passes et Auto NBA visent cette IPTV.
AuthedHttp.usePlaylist(widget.playlist.id);
return Container( return Container(
color: Colors.black, color: Colors.black,
child: Column( child: Column(
@@ -112,7 +114,7 @@ class _RecordingsTabState extends State<RecordingsTab>
_EpgGuideView(playlist: widget.playlist), _EpgGuideView(playlist: widget.playlist),
_RecordingsListView(playlist: widget.playlist), _RecordingsListView(playlist: widget.playlist),
const _SeasonPassesView(), const _SeasonPassesView(),
const _AutoNbaView(), _AutoNbaView(playlist: widget.playlist),
], ],
), ),
), ),
@@ -1752,7 +1754,9 @@ const _mois = [
]; ];
class _AutoNbaView extends StatefulWidget { class _AutoNbaView extends StatefulWidget {
const _AutoNbaView(); /// IPTV dont la règle est affichée et qui capture les matchs choisis.
final PlaylistConfig playlist;
const _AutoNbaView({required this.playlist});
@override @override
State<_AutoNbaView> createState() => _AutoNbaViewState(); State<_AutoNbaView> createState() => _AutoNbaViewState();
} }
@@ -1773,6 +1777,21 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
_load(); _load();
} }
@override
void didUpdateWidget(covariant _AutoNbaView oldWidget) {
super.didUpdateWidget(oldWidget);
// Autre IPTV : autre règle, autres matchs choisis.
if (oldWidget.playlist.id != widget.playlist.id) {
AuthedHttp.usePlaylist(widget.playlist.id);
setState(() {
_rule = null;
_games = [];
_diag = null;
});
_load();
}
}
Future<void> _load() async { Future<void> _load() async {
setState(() { setState(() {
_isLoading = _rule == null; _isLoading = _rule == null;
@@ -2427,7 +2446,8 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
'Matchs NBA en direct sur beIN Sports FR', 'Matchs NBA en direct sur beIN Sports FR · '
'enregistrés sur « ${widget.playlist.name} »',
style: GoogleFonts.fraunces( style: GoogleFonts.fraunces(
color: AppColors.onSurface54, color: AppColors.onSurface54,
fontSize: 13, fontSize: 13,
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/api/authed_http.dart';
import '../../../../core/models/playlist_config.dart'; import '../../../../core/models/playlist_config.dart';
import '../../../widgets/mobile_scaffold.dart'; import '../../../widgets/mobile_scaffold.dart';
import '../../../theme/mobile_theme.dart'; import '../../../theme/mobile_theme.dart';
@@ -26,6 +27,8 @@ class _MobileDashboardScreenState extends ConsumerState<MobileDashboardScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Toutes les requêtes /api/* de ce tableau de bord visent cette IPTV.
AuthedHttp.usePlaylist(widget.playlist.id, json: widget.playlist.toJson());
return Theme( return Theme(
data: MobileTheme.darkTheme, data: MobileTheme.darkTheme,
child: MobileScaffold( child: MobileScaffold(
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../../features/iptv/providers/xtream_provider.dart'; import '../../../../features/iptv/providers/xtream_provider.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../../core/services/playlist_api_service.dart'; import '../../../../core/services/playlist_api_service.dart';
import '../../../../core/api/authed_http.dart';
import '../../../../core/models/playlist_config.dart'; import '../../../../core/models/playlist_config.dart';
import '../../../../features/auth/providers/auth_provider.dart'; import '../../../../features/auth/providers/auth_provider.dart';
import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_colors.dart';
@@ -108,6 +109,7 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget {
return _MobilePlaylistCard( return _MobilePlaylistCard(
playlist: playlist, playlist: playlist,
onTap: () { onTap: () {
AuthedHttp.usePlaylist(playlist.id, json: playlist.toJson());
// Choix + persistance dans le même geste : l'en-tête // Choix + persistance dans le même geste : l'en-tête
// X-Playlist-Id doit partir dès la première requête. // X-Playlist-Id doit partir dès la première requête.
selectPlaylist(ref, playlist); selectPlaylist(ref, playlist);