feat: Implement a mobile-optimized IPTV settings tab including video playback, content filters, and cache management.

This commit is contained in:
Michael committed 2025-12-21 09:46:55 +01:00
1 parent af95742321
commit 3d88463c65
4 files changed
+54 -154

No files matched your search

@@ -77,8 +77,6 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
final FocusNode _deinterlaceFocusNode = FocusNode();
Timer? _controlsTimer;
Timer? _clockTimer;
String _currentTime = '';
@override
void initState() {
@@ -90,7 +88,6 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
WakelockPlus.enable();
_currentIndex = widget.initialIndex;
_startClock();
// Lock orientation
SystemChrome.setPreferredOrientations([
@@ -305,30 +302,34 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
return false;
}
// Arrow keys - Show controls or let focus navigate, NO zapping
if (key == LogicalKeyboardKey.arrowDown ||
key == LogicalKeyboardKey.channelDown) {
if (widget.streamType == StreamType.live) {
// Only switch with arrows if controls are hidden
if (key == LogicalKeyboardKey.channelDown || !_showControls) {
_playNext();
key == LogicalKeyboardKey.arrowUp ||
key == LogicalKeyboardKey.arrowLeft ||
key == LogicalKeyboardKey.arrowRight) {
if (!_showControls) {
_onUserInteraction();
return true;
}
}
return false; // Let focus system handle it if controls are visible
return false; // Let Focus system handle navigation
}
if (key == LogicalKeyboardKey.arrowUp ||
key == LogicalKeyboardKey.channelUp) {
// Channel Down = Previous Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelDown) {
if (widget.streamType == StreamType.live) {
// Only switch with arrows if controls are hidden
if (key == LogicalKeyboardKey.channelUp || !_showControls) {
_playPrevious();
_onUserInteraction();
return true;
}
}
return false; // Let focus system handle it if controls are visible
// Channel Up = Next Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelUp) {
if (widget.streamType == StreamType.live) {
_playNext();
_onUserInteraction();
return true;
}
}
if (widget.streamType != StreamType.live) {
@@ -366,22 +367,6 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
return false;
}
void _startClock() {
_updateTime();
_clockTimer =
Timer.periodic(const Duration(seconds: 1), (_) => _updateTime());
}
void _updateTime() {
if (mounted) {
final now = DateTime.now();
setState(() {
_currentTime =
"${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}";
});
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
@@ -389,7 +374,6 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
WakelockPlus.disable();
_controlsTimer?.cancel();
_clockTimer?.cancel();
_epgTimer?.cancel();
_controller?.dispose();
_xtreamService?.dispose();
@@ -564,13 +548,6 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
const Spacer(),
if (ref.watch(mobileSettingsProvider).showClock) ...[
const SizedBox(width: 16),
Text(
_currentTime,
style: const TextStyle(color: Colors.white70),
),
],
],
),
),
@@ -739,78 +716,6 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
),
),
// Deinterlace Toggle Button (Only for Live)
if (widget.streamType == StreamType.live)
Positioned(
bottom: 20, // Moved down and left to be visible and out of the way
right: 24,
child: TVFocusable(
focusNode: _deinterlaceFocusNode,
onPressed: _togglePerChannelDeinterlace,
onFocus: _resetControlsTimer,
borderRadius: BorderRadius.circular(12),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: ref
.watch(deinterlaceSettingsProvider.notifier)
.isEnabled(widget.channels != null &&
widget.channels!.isNotEmpty
? widget.channels![_currentIndex].streamId
: widget.streamId)
? AppColors.primary
: Colors.white24,
width: 2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.waves,
color: ref
.watch(deinterlaceSettingsProvider.notifier)
.isEnabled(widget.channels != null &&
widget.channels!.isNotEmpty
? widget.channels![_currentIndex].streamId
: widget.streamId)
? AppColors.primary
: Colors.white70,
size: 20,
),
const SizedBox(width: 8),
const Text(
"Désentrelacer",
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
SizedBox(
height: 24,
child: Switch(
value: ref
.watch(deinterlaceSettingsProvider.notifier)
.isEnabled(widget.channels != null &&
widget.channels!.isNotEmpty
? widget.channels![_currentIndex].streamId
: widget.streamId),
onChanged: (val) => _togglePerChannelDeinterlace(),
activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
],
),
),
),
),
// Progress Bar (VOD)
if (widget.streamType != StreamType.live)
Positioned(
@@ -810,58 +810,58 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
return true;
}
// Left Arrow - Seek back 10 seconds
// Left Arrow - Show controls or navigate, seek in VOD when hidden
if (key == LogicalKeyboardKey.arrowLeft) {
if (_showControls) return false; // Let Focus handle navigation
if (!_showControls) {
if (widget.streamType != StreamType.live) {
final newPos = _position - const Duration(seconds: 5);
_player.seek(newPos < Duration.zero ? Duration.zero : newPos);
_onUserInteraction();
}
_onUserInteraction();
return true;
}
return false; // Let Focus handle navigation
}
// Right Arrow - Seek forward 10 seconds
// Right Arrow - Show controls or navigate, seek in VOD when hidden
if (key == LogicalKeyboardKey.arrowRight) {
if (_showControls) return false; // Let Focus handle navigation
if (!_showControls) {
if (widget.streamType != StreamType.live) {
final newPos = _position + const Duration(seconds: 5);
if (newPos < _duration) {
_player.seek(newPos);
}
_onUserInteraction();
}
_onUserInteraction();
return true;
}
return false; // Let Focus handle navigation
}
// Up Arrow - Previous channel (Live only)
// Up/Down Arrow - Show controls or navigate UI, NO zapping
if (key == LogicalKeyboardKey.arrowUp ||
key == LogicalKeyboardKey.channelUp) {
key == LogicalKeyboardKey.arrowDown) {
if (!_showControls) {
_onUserInteraction();
return true;
}
return false; // Let Focus handle navigation
}
// Channel Down = Previous Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelDown) {
if (widget.streamType == StreamType.live && widget.channels != null) {
if (key == LogicalKeyboardKey.channelUp || !_showControls) {
_playPrevious();
return true;
}
}
return false; // Let focus handle navigation if controls shown
}
// Down Arrow - Next channel (Live) or Cycle Audio (VOD)
if (key == LogicalKeyboardKey.arrowDown ||
key == LogicalKeyboardKey.channelDown) {
// Channel Up = Next Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelUp) {
if (widget.streamType == StreamType.live && widget.channels != null) {
if (key == LogicalKeyboardKey.channelDown || !_showControls) {
_playNext();
return true;
}
} else if (widget.streamType != StreamType.live &&
key == LogicalKeyboardKey.arrowDown) {
_cycleAudioTrack();
return true;
}
return false; // Let focus handle navigation if controls shown
}
// Cycle Aspect Ratio - secret key 'A' or digit 1
@@ -102,7 +102,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
),
),
Text(
'Version 1.3.4',
'Version 1.3.9',
style: GoogleFonts.inter(
fontSize: 13,
color: AppColors.textSecondary,
@@ -145,18 +145,6 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
},
),
_buildSettingItem(
icon: Icons.line_weight,
title: 'Désentrelacement',
subtitle: settings.deinterlace
? 'Activé (Recommandé pour TV en direct)'
: 'Désactivé',
value: settings.deinterlace ? 'Oui' : 'Non',
onTap: () => ref
.read(mobileSettingsProvider.notifier)
.toggleDeinterlace(!settings.deinterlace),
),
_buildSettingItem(
icon: Icons.speed,
title: 'Buffer (Cache)',
@@ -377,8 +365,10 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
@@ -458,16 +448,21 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
);
}
void _showKeyboardDialog(String label, TextEditingController controller,
ValueChanged<String> onChanged) {
void _showKeyboardDialog(
String label,
TextEditingController controller,
ValueChanged<String> onChanged,
) {
final tempController = TextEditingController(text: controller.text);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColors.surface,
title: Text('Filtres $label',
style: const TextStyle(color: AppColors.textPrimary)),
title: Text(
'Filtres $label',
style: const TextStyle(color: AppColors.textPrimary),
),
content: TextField(
controller: tempController,
autofocus: true,
+1 -1
View File
@@ -1,7 +1,7 @@
name: xtremflow
description: High-performance IPTV Web Application with Xtream Codes API
publish_to: 'none'
version: 1.0.0+1
version: 1.3.9+4
environment:
sdk: '>=3.0.0 <4.0.0'