feat: implement IPTV player screen with iframe-based stream support and recordings tab widget

This commit is contained in:
Michael committed 2026-04-01 20:19:40 +02:00
1 parent 77dc9053bb
commit 6a4a642611
13 files changed
+218 -120

No files matched your search

@@ -29,7 +29,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
'Movies',
'Series',
'Recordings',
'Settings'
'Settings',
];
final List<IconData> _icons = [
Icons.live_tv_rounded,
+10 -4
View File
@@ -196,9 +196,13 @@ class _EpgGridScreenState extends ConsumerState<EpgGridScreen>
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(day, style: const TextStyle(fontSize: 11)),
Text(dateNum,
style: const TextStyle(
fontSize: 13, fontWeight: FontWeight.bold)),
Text(
dateNum,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
);
@@ -257,7 +261,9 @@ class _EpgGridScreenState extends ConsumerState<EpgGridScreen>
child: Text(
'$hour:00',
style: const TextStyle(
fontSize: 11, fontWeight: FontWeight.bold),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
);
+2 -1
View File
@@ -506,7 +506,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
_buildInlineEpg(),
const SizedBox(height: 12),
const Divider(
color: Colors.white10),
color: Colors.white10,
),
const SizedBox(height: 8),
// Control Buttons
Row(
@@ -183,8 +183,9 @@ class XtreamService {
/// Get all live TV channels grouped by category
///
/// Returns a Map where key is category name and value is list of channels
Future<Map<String, List<Channel>>> getLiveChannels(
{bool refresh = false}) async {
Future<Map<String, List<Channel>>> getLiveChannels({
bool refresh = false,
}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
final options = refresh
+44 -13
View File
@@ -7,9 +7,10 @@ import '../../../core/models/iptv_models.dart';
import '../../../core/models/playlist_config.dart';
import '../providers/xtream_provider.dart';
import '../providers/settings_provider.dart';
import '../screens/player_screen.dart';
// ═══════════════════════════════════════════════════════════════════════════
// ENTRÉE — Onglet "TV & Enregistrements" avec 3 sous-onglets
// ENTRÉE — Onglet "Enregistrements"
// ═══════════════════════════════════════════════════════════════════════════
class RecordingsTab extends StatefulWidget {
@@ -27,7 +28,7 @@ class _RecordingsTabState extends State<RecordingsTab>
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController = TabController(length: 1, vsync: this);
}
@override
@@ -73,15 +74,10 @@ class _RecordingsTabState extends State<RecordingsTab>
),
unselectedLabelStyle: GoogleFonts.outfit(fontSize: 14),
tabs: const [
Tab(icon: Icon(Icons.grid_view, size: 18), text: 'Guide TV'),
Tab(
icon: Icon(Icons.fiber_manual_record, size: 18),
text: 'Enregistrements',
),
Tab(
icon: Icon(Icons.repeat, size: 18),
text: 'Season Passes',
),
],
),
],
@@ -89,11 +85,8 @@ class _RecordingsTabState extends State<RecordingsTab>
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_EpgGuideView(playlist: widget.playlist),
const _RecordingsListView(),
const _SeasonPassesView(),
_RecordingsListView(playlist: widget.playlist),
],
),
),
@@ -298,7 +291,9 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
decoration: InputDecoration(
hintText: 'Rechercher...',
hintStyle: const TextStyle(
color: Colors.white38, fontSize: 13),
color: Colors.white38,
fontSize: 13,
),
prefixIcon: const Icon(
Icons.search,
color: Colors.white38,
@@ -765,7 +760,8 @@ class _ProgrammeCard extends StatelessWidget {
// ═══════════════════════════════════════════════════════════════════════════
class _RecordingsListView extends StatefulWidget {
const _RecordingsListView();
final PlaylistConfig playlist;
const _RecordingsListView({required this.playlist});
@override
State<_RecordingsListView> createState() => _RecordingsListViewState();
}
@@ -821,6 +817,31 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
_fetchRecordings();
}
Future<void> _playRecording(BuildContext context, Map<String, dynamic> rec) async {
final streamUrl = rec['stream_url'] as String? ?? '';
final title = rec['title'] as String? ?? 'Enregistrement';
if (streamUrl.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URL du flux indisponible')),
);
return;
}
if (!mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => PlayerScreen(
streamId: streamUrl,
title: title,
playlist: widget.playlist,
streamType: StreamType.vod,
containerExtension: 'ts',
),
),
);
}
Future<void> _showLogs(String id, String title) async {
try {
final response = await http.get(Uri.parse('/api/recordings/logs/$id'));
@@ -1033,6 +1054,16 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
rec['title'] ?? '',
),
),
if (status == 'completed')
IconButton(
icon: const Icon(
Icons.play_circle_outline,
color: Colors.greenAccent,
size: 20,
),
tooltip: 'Lecture',
onPressed: () => _playRecording(context, rec),
),
IconButton(
icon: const Icon(
Icons.description_outlined,