mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Rafraîchissement automatique de la page Enregistrements
- Nouveau bus de notification (recordingsRefreshBus) : la liste se recharge immédiatement dès qu'un enregistrement est créé, planifié ou arrêté depuis n'importe où dans l'app (guide EPG, modal, widget rapide) - Polling en arrière-plan : 5 s quand un enregistrement est en cours ou planifié, 20 s sinon, pour suivre les changements de statut sans clic - Rafraîchissement silencieux (pas de spinner ni de vidage de liste) pour éviter le clignotement ; les erreurs transitoires ne remplacent pas la liste - Indicateur « Suivi auto » avec l'heure de dernière actualisation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
This commit is contained in:
5 files changed
+103
-11
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# 📝 Changelog - XtremFlow Optimisations
|
||||
|
||||
## Non publié
|
||||
|
||||
### 📺 Enregistrements
|
||||
- La liste des enregistrements se met à jour automatiquement : rafraîchissement immédiat dès qu'un enregistrement est créé/arrêté n'importe où dans l'app (guide EPG, modal, widget rapide), et polling en arrière-plan (5 s quand un enregistrement est en cours ou planifié, 20 s sinon) pour suivre les statuts sans clic manuel
|
||||
- Indicateur « Suivi auto » avec heure de dernière actualisation dans l'onglet Enregistrements
|
||||
|
||||
## Version 1.2 - Security, Streaming & Design Overhaul (10 Juin 2026)
|
||||
|
||||
### 🔐 Sécurité
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Bus de notification global pour la liste des enregistrements.
|
||||
///
|
||||
/// L'onglet Enregistrements vit dans un IndexedStack : il n'est construit
|
||||
/// qu'une seule fois au lancement. Tout code qui crée/modifie un
|
||||
/// enregistrement (modal d'enregistrement, guide EPG, ...) appelle
|
||||
/// [notifyRecordingsChanged] pour que la liste se recharge immédiatement,
|
||||
/// sans attendre le prochain cycle de polling.
|
||||
final ValueNotifier<int> recordingsRefreshBus = ValueNotifier<int>(0);
|
||||
|
||||
void notifyRecordingsChanged() {
|
||||
recordingsRefreshBus.value++;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import '../../../core/models/iptv_models.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../../../core/widgets/tv_focusable_card.dart';
|
||||
import '../providers/recordings_refresh.dart';
|
||||
|
||||
class RecordingModal extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@@ -59,6 +60,7 @@ class _RecordingModalState extends State<RecordingModal> {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
notifyRecordingsChanged();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enregistrement programmé !')),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -6,6 +7,7 @@ import '../../../core/api/authed_http.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../providers/recordings_refresh.dart';
|
||||
import '../providers/xtream_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../screens/player_screen.dart';
|
||||
@@ -603,6 +605,7 @@ class _ProgrammeCard extends StatelessWidget {
|
||||
'end_time': end.toIso8601String(),
|
||||
}),
|
||||
);
|
||||
if (response.statusCode == 200) notifyRecordingsChanged();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -733,37 +736,83 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
List<dynamic> _recordings = [];
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
Timer? _pollTimer;
|
||||
DateTime? _lastRefresh;
|
||||
|
||||
// Polling rapproché quand un enregistrement est en cours, plus espacé sinon.
|
||||
static const _activeInterval = Duration(seconds: 5);
|
||||
static const _idleInterval = Duration(seconds: 20);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
recordingsRefreshBus.addListener(_onExternalChange);
|
||||
_fetchRecordings();
|
||||
}
|
||||
|
||||
Future<void> _fetchRecordings() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
@override
|
||||
void dispose() {
|
||||
recordingsRefreshBus.removeListener(_onExternalChange);
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Un enregistrement vient d'être créé/modifié ailleurs dans l'app.
|
||||
void _onExternalChange() {
|
||||
_fetchRecordings(silent: true);
|
||||
}
|
||||
|
||||
bool get _hasActiveOrPending => _recordings.any((rec) {
|
||||
final status = rec is Map ? rec['status'] as String? : null;
|
||||
return status == 'recording' || status == 'scheduled';
|
||||
});
|
||||
|
||||
void _scheduleNextPoll() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = Timer(
|
||||
_hasActiveOrPending ? _activeInterval : _idleInterval,
|
||||
() => _fetchRecordings(silent: true),
|
||||
);
|
||||
}
|
||||
|
||||
/// [silent] : recharge en arrière-plan sans spinner ni vidage de la liste,
|
||||
/// pour que le suivi automatique ne fasse pas clignoter l'écran.
|
||||
Future<void> _fetchRecordings({bool silent = false}) async {
|
||||
if (!silent) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
try {
|
||||
final response = await AuthedHttp.get(Uri.parse('/api/recordings'));
|
||||
if (!mounted) return;
|
||||
if (response.statusCode == 200) {
|
||||
setState(() {
|
||||
final decoded = json.decode(response.body);
|
||||
_recordings = decoded is List ? decoded : [];
|
||||
_isLoading = false;
|
||||
_error = null;
|
||||
_lastRefresh = DateTime.now();
|
||||
});
|
||||
} else {
|
||||
} else if (!silent) {
|
||||
setState(() {
|
||||
_error = 'Erreur ${response.statusCode}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = '$e';
|
||||
_isLoading = false;
|
||||
});
|
||||
if (!mounted) return;
|
||||
// En mode silencieux on garde la liste affichée plutôt que
|
||||
// de remplacer l'écran par une erreur transitoire.
|
||||
if (!silent) {
|
||||
setState(() {
|
||||
_error = '$e';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (mounted) _scheduleNextPoll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,10 +945,27 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.autorenew,
|
||||
color: _hasActiveOrPending
|
||||
? AppColors.success
|
||||
: AppColors.onSurface38,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_lastRefresh == null
|
||||
? 'Suivi automatique'
|
||||
: 'Suivi auto · ${_lastRefresh!.hour.toString().padLeft(2, '0')}:${_lastRefresh!.minute.toString().padLeft(2, '0')}:${_lastRefresh!.second.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface38,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, color: AppColors.onSurfaceVariant),
|
||||
onPressed: _fetchRecordings,
|
||||
tooltip: 'Rafraîchir',
|
||||
tooltip: 'Rafraîchir maintenant',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../providers/recordings_refresh.dart';
|
||||
|
||||
class SimpleRecordingWidget extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@@ -43,6 +44,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
notifyRecordingsChanged();
|
||||
setState(() {
|
||||
_isRecording = true;
|
||||
_status = '🔴 Recording for $minutes min';
|
||||
@@ -79,6 +81,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
notifyRecordingsChanged();
|
||||
setState(() => _status = '⏰ Scheduled for ${_startTime.toLocal()}');
|
||||
|
||||
if (mounted) {
|
||||
@@ -101,6 +104,7 @@ class _SimpleRecordingWidgetState extends State<SimpleRecordingWidget> {
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
notifyRecordingsChanged();
|
||||
setState(() {
|
||||
_isRecording = false;
|
||||
_status = 'Stopped';
|
||||
|
||||
Reference in new issue
Block a user