mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Merge remote-tracking branch 'origin/main' into claude/fonctionnalites-frontend
# Conflicts: # CHANGELOG.md
This commit is contained in:
10 files changed
+740
-187
No files matched your search
@@ -0,0 +1,42 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'authed_http.dart';
|
||||
|
||||
/// Point d'entrée unique pour créer un enregistrement via POST /api/recordings.
|
||||
///
|
||||
/// Le backend exige désormais des dates ISO-8601 AVEC fuseau (suffixe 'Z' ou
|
||||
/// offset) et rejette les dates naïves en 400 : trois conventions d'envoi
|
||||
/// coexistaient dans l'app (UTC, local naïf, UTC naïf), d'où des
|
||||
/// enregistrements décalés de 1-2 h selon l'écran utilisé.
|
||||
///
|
||||
/// [wallClockIsUtc] : les horaires issus de l'EPG sont des heures UTC
|
||||
/// « naïves » (parsées sans fuseau par Dart mais comparées à `now.toUtc()`
|
||||
/// partout dans l'app). true les re-tague en UTC sans décalage ; false (par
|
||||
/// défaut) convertit depuis l'heure locale réelle (cas d'un DateTime.now()).
|
||||
Future<http.Response> postRecording({
|
||||
required String channelId,
|
||||
required String title,
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
String? streamUrl,
|
||||
bool wallClockIsUtc = false,
|
||||
}) {
|
||||
DateTime asUtc(DateTime d) {
|
||||
if (d.isUtc) return d;
|
||||
return wallClockIsUtc
|
||||
? DateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second)
|
||||
: d.toUtc();
|
||||
}
|
||||
|
||||
return AuthedHttp.post(
|
||||
Uri.parse('/api/recordings'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'channel_id': channelId,
|
||||
'stream_url': streamUrl ?? '/api/live/$channelId.ts',
|
||||
'title': title,
|
||||
'start_time': asUtc(start).toIso8601String(),
|
||||
'end_time': asUtc(end).toIso8601String(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/api/authed_http.dart';
|
||||
import '../../../core/api/recording_requests.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
@@ -43,20 +42,15 @@ class _RecordingModalState extends State<RecordingModal> {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final endTime = _startTime.add(Duration(minutes: _durationMinutes));
|
||||
|
||||
|
||||
try {
|
||||
// Utilisation d'une URL relative en Web (ou d'une configuration pour autres plateformes)
|
||||
final response = await AuthedHttp.post(
|
||||
Uri.parse('/api/recordings'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'channel_id': widget.channel.streamId,
|
||||
'stream_url': '/api/live/${widget.channel.streamId}.ts',
|
||||
'title': widget.channel.name,
|
||||
// Forcer UTC pour éviter le décalage +01:00 (France) vs UTC (serveur Docker)
|
||||
'start_time': _startTime.toUtc().toIso8601String(),
|
||||
'end_time': endTime.toUtc().toIso8601String(),
|
||||
}),
|
||||
// _startTime est une vraie heure locale (pickers) : postRecording la
|
||||
// convertit en UTC — seule convention acceptée par le backend.
|
||||
final response = await postRecording(
|
||||
channelId: widget.channel.streamId,
|
||||
title: widget.channel.name,
|
||||
start: _startTime,
|
||||
end: endTime,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/api/authed_http.dart';
|
||||
import '../../../core/api/recording_requests.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
@@ -649,16 +650,15 @@ class _ProgrammeCard extends StatelessWidget {
|
||||
DateTime end,
|
||||
) async {
|
||||
try {
|
||||
final response = await AuthedHttp.post(
|
||||
Uri.parse('/api/recordings'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'channel_id': channel.streamId,
|
||||
'stream_url': '/api/live/${channel.streamId}.ts',
|
||||
'title': title,
|
||||
'start_time': start.toIso8601String(),
|
||||
'end_time': end.toIso8601String(),
|
||||
}),
|
||||
// Les horaires EPG sont des heures UTC naïves : wallClockIsUtc les
|
||||
// re-tague sans décalage (l'ancien envoi naïf était interprété dans le
|
||||
// fuseau du serveur → enregistrement décalé de 1-2 h).
|
||||
final response = await postRecording(
|
||||
channelId: channel.streamId,
|
||||
title: title,
|
||||
start: start,
|
||||
end: end,
|
||||
wallClockIsUtc: true,
|
||||
);
|
||||
if (response.statusCode == 200) notifyRecordingsChanged();
|
||||
if (context.mounted) {
|
||||
@@ -1001,6 +1001,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
'recording' => AppColors.live,
|
||||
'completed' => AppColors.success,
|
||||
'failed' => AppColors.warning,
|
||||
'cancelled' => AppColors.onSurface38,
|
||||
_ => AppColors.primaryContainer,
|
||||
};
|
||||
|
||||
@@ -1009,9 +1010,20 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
'recording' => '● En cours',
|
||||
'completed' => 'Terminé',
|
||||
'failed' => 'Échoué',
|
||||
'cancelled' => 'Annulé',
|
||||
_ => status,
|
||||
};
|
||||
|
||||
String _fmtSize(int bytes) {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} Go';
|
||||
}
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(0)} Mo';
|
||||
}
|
||||
return '${(bytes / 1024).toStringAsFixed(0)} Ko';
|
||||
}
|
||||
|
||||
String _fmtDate(dynamic raw) {
|
||||
if (raw == null) return '?';
|
||||
try {
|
||||
@@ -1138,12 +1150,34 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${_fmtDate(rec['start_time'])} → ${_fmtDate(rec['end_time'])}',
|
||||
'${_fmtDate(rec['start_time'])} → ${_fmtDate(rec['end_time'])}'
|
||||
'${rec['file_size_bytes'] is int ? ' · ${_fmtSize(rec['file_size_bytes'] as int)}' : ''}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface54,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (status == 'recording' &&
|
||||
rec['progress_pct'] is int) ...[
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(3),
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
(rec['progress_pct'] as int) /
|
||||
100,
|
||||
minHeight: 4,
|
||||
backgroundColor: AppColors
|
||||
.onSurface
|
||||
.withOpacity(0.1),
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation(
|
||||
AppColors.live,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (rec['error_reason'] != null)
|
||||
Text(
|
||||
'⚠ ${rec['error_reason']}',
|
||||
|
||||
Reference in new issue
Block a user