mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
fix(epg): use /api/epg/ backend for full guide (48h), fix _ProgrammeCard for Map format, remove stale EpgEntry code
This commit is contained in:
1 parent
1e1687e160
commit
37945b513e
1 file changed
+68
-51
@@ -115,7 +115,8 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
|
|||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
Channel? _selectedChannel;
|
Channel? _selectedChannel;
|
||||||
List<EpgEntry> _programmes = [];
|
// Map-based pour correspondre au format retourné par /api/epg/<id>
|
||||||
|
List<Map<String, dynamic>> _programmes = [];
|
||||||
bool _loadingEpg = false;
|
bool _loadingEpg = false;
|
||||||
String _epgError = '';
|
String _epgError = '';
|
||||||
|
|
||||||
@@ -135,7 +136,8 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Charge l'EPG via XtreamService (identique à EPGWidget)
|
/// Charge le guide EPG complet via le backend /api/epg/<channelId>
|
||||||
|
/// Le backend appelle get_epg (48h de programmes) et décode le base64
|
||||||
Future<void> _loadEpg(Channel ch) async {
|
Future<void> _loadEpg(Channel ch) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedChannel = ch;
|
_selectedChannel = ch;
|
||||||
@@ -145,15 +147,26 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Utiliser directement XtreamService comme EPGWidget — passe par /api/xtream/ proxy
|
final response =
|
||||||
final service = ref.read(xtreamServiceProvider(widget.playlist));
|
await http.get(Uri.parse('/api/epg/${ch.streamId}'));
|
||||||
final epgEntries = await service.getShortEpg(ch.streamId);
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
if (response.statusCode == 200) {
|
||||||
_programmes = epgEntries;
|
final data = json.decode(response.body) as Map<String, dynamic>;
|
||||||
_loadingEpg = false;
|
final list =
|
||||||
});
|
(data['programmes'] as List<dynamic>? ?? [])
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.toList();
|
||||||
|
setState(() {
|
||||||
|
_programmes = list;
|
||||||
|
_loadingEpg = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_epgError =
|
||||||
|
'Erreur ${response.statusCode}: ${response.body}';
|
||||||
|
_loadingEpg = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -383,7 +396,6 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
|
|||||||
_ProgrammeCard(
|
_ProgrammeCard(
|
||||||
programme: _programmes[i],
|
programme: _programmes[i],
|
||||||
channel: _selectedChannel!,
|
channel: _selectedChannel!,
|
||||||
playlist: widget.playlist,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -401,53 +413,58 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
class _ProgrammeCard extends StatelessWidget {
|
class _ProgrammeCard extends StatelessWidget {
|
||||||
final EpgEntry programme;
|
final Map<String, dynamic> programme;
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
final PlaylistConfig playlist;
|
|
||||||
|
|
||||||
const _ProgrammeCard({
|
const _ProgrammeCard({
|
||||||
required this.programme,
|
required this.programme,
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required this.playlist,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
String get _start => programme['start'] as String? ?? '';
|
||||||
|
String get _end => programme['end'] as String? ?? '';
|
||||||
|
String get _title => programme['title'] as String? ?? '';
|
||||||
|
String get _description => programme['description'] as String? ?? '';
|
||||||
|
|
||||||
String _fmt(String raw) {
|
String _fmt(String raw) {
|
||||||
|
if (raw.isEmpty) return '';
|
||||||
try {
|
try {
|
||||||
final dt = DateTime.parse(raw).toLocal();
|
// Format Xtream : "2026-03-09 09:00:00" ou ISO "2026-03-09T09:00:00Z"
|
||||||
|
final dt = DateTime.parse(raw.replaceFirst(' ', 'T')).toLocal();
|
||||||
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return raw.length >= 16 ? raw.substring(11, 16) : raw;
|
return raw.length >= 16 ? raw.substring(11, 16) : raw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isNow {
|
DateTime? _parseTime(String raw) {
|
||||||
|
if (raw.isEmpty) return null;
|
||||||
try {
|
try {
|
||||||
final s = DateTime.parse(programme.start);
|
return DateTime.parse(raw.replaceFirst(' ', 'T')).toUtc();
|
||||||
final e = DateTime.parse(programme.end);
|
|
||||||
final now = DateTime.now().toUtc();
|
|
||||||
return now.isAfter(s) && now.isBefore(e);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _isNow {
|
||||||
|
final s = _parseTime(_start);
|
||||||
|
final e = _parseTime(_end);
|
||||||
|
if (s == null || e == null) return false;
|
||||||
|
final now = DateTime.now().toUtc();
|
||||||
|
return now.isAfter(s) && now.isBefore(e);
|
||||||
|
}
|
||||||
|
|
||||||
bool get _isPast {
|
bool get _isPast {
|
||||||
try {
|
final e = _parseTime(_end);
|
||||||
return DateTime.now().toUtc().isAfter(DateTime.parse(programme.end));
|
if (e == null) return false;
|
||||||
} catch (_) {
|
return DateTime.now().toUtc().isAfter(e);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _record(BuildContext context) {
|
void _record(BuildContext context) {
|
||||||
try {
|
final s = _parseTime(_start);
|
||||||
final start = DateTime.parse(programme.start).toUtc();
|
final e = _parseTime(_end);
|
||||||
final end = DateTime.parse(programme.end).toUtc();
|
if (s == null || e == null) return;
|
||||||
_showConfirm(context, programme.title, start, end);
|
_showConfirm(context, _title, s, e);
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(SnackBar(content: Text('Erreur: $e')));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showConfirm(
|
void _showConfirm(
|
||||||
@@ -468,20 +485,20 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
Text(title.isEmpty ? channel.name : title,
|
||||||
style: GoogleFonts.outfit(
|
style: GoogleFonts.outfit(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16)),
|
fontSize: 16)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'${_fmt(programme.start)} → ${_fmt(programme.end)}',
|
'${_fmt(_start)} → ${_fmt(_end)}',
|
||||||
style: const TextStyle(color: Colors.white70),
|
style: const TextStyle(color: Colors.white70),
|
||||||
),
|
),
|
||||||
if (programme.description.isNotEmpty) ...[
|
if (_description.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
programme.description,
|
_description,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
||||||
@@ -498,7 +515,8 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
await _saveRecording(context, title, start, end);
|
await _saveRecording(
|
||||||
|
context, title.isEmpty ? channel.name : title, start, end);
|
||||||
},
|
},
|
||||||
child: const Text('🔴 Enregistrer'),
|
child: const Text('🔴 Enregistrer'),
|
||||||
),
|
),
|
||||||
@@ -510,13 +528,12 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
Future<void> _saveRecording(
|
Future<void> _saveRecording(
|
||||||
BuildContext context, String title, DateTime start, DateTime end) async {
|
BuildContext context, String title, DateTime start, DateTime end) async {
|
||||||
try {
|
try {
|
||||||
final streamUrl = '/api/live/${channel.streamId}.ts';
|
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse('/api/recordings'),
|
Uri.parse('/api/recordings'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
'channel_id': channel.streamId,
|
'channel_id': channel.streamId,
|
||||||
'stream_url': streamUrl,
|
'stream_url': '/api/live/${channel.streamId}.ts',
|
||||||
'title': title,
|
'title': title,
|
||||||
'start_time': start.toIso8601String(),
|
'start_time': start.toIso8601String(),
|
||||||
'end_time': end.toIso8601String(),
|
'end_time': end.toIso8601String(),
|
||||||
@@ -564,7 +581,7 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
||||||
leading: SizedBox(
|
leading: SizedBox(
|
||||||
width: 48,
|
width: 52,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -583,7 +600,7 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
_fmt(programme.start),
|
_fmt(_start),
|
||||||
style: GoogleFonts.outfit(
|
style: GoogleFonts.outfit(
|
||||||
color: isPast ? Colors.white24 : Colors.white70,
|
color: isPast ? Colors.white24 : Colors.white70,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -592,15 +609,15 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (!isNow)
|
if (!isNow)
|
||||||
Text(
|
Text(
|
||||||
_fmt(programme.end),
|
_fmt(_end),
|
||||||
style: GoogleFonts.outfit(
|
style:
|
||||||
color: Colors.white24, fontSize: 10),
|
GoogleFonts.outfit(color: Colors.white24, fontSize: 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
programme.title.isEmpty ? '—' : programme.title,
|
_title.isEmpty ? '—' : _title,
|
||||||
style: GoogleFonts.outfit(
|
style: GoogleFonts.outfit(
|
||||||
color: isPast ? Colors.white38 : Colors.white,
|
color: isPast ? Colors.white38 : Colors.white,
|
||||||
fontWeight: isNow ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isNow ? FontWeight.bold : FontWeight.normal,
|
||||||
@@ -608,13 +625,12 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: programme.description.isNotEmpty
|
subtitle: _description.isNotEmpty
|
||||||
? Text(
|
? Text(
|
||||||
programme.description,
|
_description,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style:
|
style: const TextStyle(color: Colors.white38, fontSize: 11),
|
||||||
const TextStyle(color: Colors.white38, fontSize: 11),
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
trailing: !isPast
|
trailing: !isPast
|
||||||
@@ -630,6 +646,7 @@ class _ProgrammeCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// ONGLET 2 — ENREGISTREMENTS
|
// ONGLET 2 — ENREGISTREMENTS
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in new issue
Block a user