mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-20 13:38:05 +02:00
feat: implement IPTV player screen with iframe-based stream support and recordings tab widget
This commit is contained in:
1 parent
77dc9053bb
commit
6a4a642611
13 files changed
+218
-120
No files matched your search
@@ -295,7 +295,8 @@ class RuntimeOptimizations {
|
||||
hasHighPerformanceDevice = totalMemoryMb > 4000 && storageFreeMb > 5000;
|
||||
|
||||
print(
|
||||
'[Device Calibration] Memory: ${freeMemoryMb}MB, Storage: ${storageFreeMb}MB');
|
||||
'[Device Calibration] Memory: ${freeMemoryMb}MB, Storage: ${storageFreeMb}MB',
|
||||
);
|
||||
}
|
||||
|
||||
/// Get dynamic cache size based on available memory
|
||||
|
||||
@@ -58,7 +58,9 @@ class GlassContainer extends StatelessWidget {
|
||||
// BackdropFilter is extremely expensive in Flutter Web on mobile Safari.
|
||||
// On mobile web (width < 600), skip the blur entirely.
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final useBlur = blur > 0 && !(Theme.of(context).platform == TargetPlatform.iOS && screenWidth < 600);
|
||||
final useBlur = blur > 0 &&
|
||||
!(Theme.of(context).platform == TargetPlatform.iOS &&
|
||||
screenWidth < 600);
|
||||
|
||||
final inner = Container(
|
||||
padding: padding,
|
||||
@@ -157,10 +159,12 @@ class _GlassCardState extends State<GlassCard>
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.04).animate(
|
||||
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault));
|
||||
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
|
||||
);
|
||||
|
||||
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
|
||||
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault));
|
||||
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -285,8 +285,10 @@ class _HeroCarouselState extends State<HeroCarousel>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.play_arrow,
|
||||
color: AppColors.textPrimary),
|
||||
const Icon(
|
||||
Icons.play_arrow,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Play',
|
||||
|
||||
@@ -29,7 +29,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
'Movies',
|
||||
'Series',
|
||||
'Recordings',
|
||||
'Settings'
|
||||
'Settings',
|
||||
];
|
||||
final List<IconData> _icons = [
|
||||
Icons.live_tv_rounded,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -48,8 +48,10 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Loading channels...',
|
||||
style: TextStyle(color: Colors.white70)),
|
||||
const Text(
|
||||
'Loading channels...',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FutureBuilder(
|
||||
future: Future.delayed(const Duration(seconds: 8)),
|
||||
@@ -57,7 +59,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return TextButton(
|
||||
onPressed: () => ref.refresh(
|
||||
liveChannelsByPlaylistProvider(widget.playlist)),
|
||||
liveChannelsByPlaylistProvider(widget.playlist),
|
||||
),
|
||||
child: const Text('Taking too long? Tap to retry'),
|
||||
);
|
||||
}
|
||||
@@ -75,15 +78,20 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
Text('Failed to load channels',
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'Failed to load channels',
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('$e',
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
textAlign: TextAlign.center),
|
||||
Text(
|
||||
'$e',
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref
|
||||
|
||||
@@ -42,50 +42,50 @@ class MobileScaffold extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: const Border(
|
||||
top: BorderSide(
|
||||
color: AppColors.border,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: currentIndex,
|
||||
onTap: onIndexChanged,
|
||||
backgroundColor: Colors.transparent,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
selectedItemColor: AppColors.primary,
|
||||
unselectedItemColor: AppColors.textSecondary,
|
||||
selectedLabelStyle:
|
||||
GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 11),
|
||||
unselectedLabelStyle:
|
||||
GoogleFonts.inter(fontWeight: FontWeight.w500, fontSize: 11),
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.live_tv_rounded),
|
||||
label: 'Live',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.movie_outlined),
|
||||
activeIcon: Icon(Icons.movie_rounded),
|
||||
label: 'Movies',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.video_library_outlined),
|
||||
activeIcon: Icon(Icons.video_library_rounded),
|
||||
label: 'Series',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings_rounded),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: AppColors.border,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: currentIndex,
|
||||
onTap: onIndexChanged,
|
||||
backgroundColor: Colors.transparent,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
selectedItemColor: AppColors.primary,
|
||||
unselectedItemColor: AppColors.textSecondary,
|
||||
selectedLabelStyle:
|
||||
GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 11),
|
||||
unselectedLabelStyle:
|
||||
GoogleFonts.inter(fontWeight: FontWeight.w500, fontSize: 11),
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.live_tv_rounded),
|
||||
label: 'Live',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.movie_outlined),
|
||||
activeIcon: Icon(Icons.movie_rounded),
|
||||
label: 'Movies',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.video_library_outlined),
|
||||
activeIcon: Icon(Icons.video_library_rounded),
|
||||
label: 'Series',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings_rounded),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user