feat: Implement MobileMoviesTab for displaying IPTV movies with category browsing and search functionality.

This commit is contained in:
Michael committed 2026-03-27 12:10:23 +01:00
1 parent 0010fe12ba
commit f85f43e42f
17 files changed
+268 -136

No files matched your search

@@ -147,7 +147,8 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
}
Future<void> _updateEPG() async {
if (widget.streamType != model.StreamType.live || _xtreamService == null) return;
if (widget.streamType != model.StreamType.live || _xtreamService == null)
return;
try {
final currentChannelId =
widget.channels?[_currentIndex].streamId ?? widget.streamId;
@@ -198,10 +199,12 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
);
_controller = controller;
await controller.initialize().timeout(const Duration(seconds: 15),
onTimeout: () {
throw Exception('Connection timed out');
});
await controller.initialize().timeout(
const Duration(seconds: 15),
onTimeout: () {
throw Exception('Connection timed out');
},
);
if (!mounted) {
await controller.dispose();
@@ -221,11 +224,12 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
_resetControlsTimer();
controller.addListener(_videoListener);
} catch (e) {
if (mounted && currentLoadId == _loadId)
if (mounted && currentLoadId == _loadId) {
setState(() {
_isLoading = false;
_errorMessage = "Playback Error: $e";
});
}
}
}
@@ -233,8 +237,9 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
if (_controller == null || !mounted) return;
final value = _controller!.value;
if (value.isPlaying != _isPlaying)
if (value.isPlaying != _isPlaying) {
setState(() => _isPlaying = value.isPlaying);
}
// Stabilization Masking: spinner stays until 1.5s of smooth play
// Fix: If playing, ignore isBuffering to prevent infinite spinner on some TS streams
@@ -302,16 +307,18 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
final key = event.logicalKey;
if (key == LogicalKeyboardKey.channelUp) {
if (widget.channels != null)
if (widget.channels != null) {
_switchChannel((_currentIndex + 1) % widget.channels!.length);
}
return true;
}
if (key == LogicalKeyboardKey.channelDown) {
if (widget.channels != null)
if (widget.channels != null) {
_switchChannel(
(_currentIndex - 1 + widget.channels!.length) %
widget.channels!.length,
);
}
return true;
}
@@ -393,7 +400,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
borderRadius: BorderRadius.circular(50),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: Colors.black45,
shape: BoxShape.circle,
),
@@ -408,7 +415,9 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
if (settings.showClock)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(20),
@@ -460,9 +469,10 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
Widget _buildAspectRatioWrapper() {
final mode = ref.read(mobileSettingsProvider).aspectRatioMode;
if (mode == 'fill')
if (mode == 'fill') {
return SizedBox.expand(child: VideoPlayer(_controller!));
if (mode == 'cover')
}
if (mode == 'cover') {
return SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
@@ -473,6 +483,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
),
),
);
}
return AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
@@ -16,7 +16,7 @@ import 'package:xtremobile/features/iptv/screens/lite_player_screen.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:xtremobile/core/utils/device_info.dart';
/// Stream type enum for player
/// Stream type enum for player
// (Moved to global or imported from iptv_models if available)
/// Native video player screen for Android/iOS
@@ -165,7 +165,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
'MediaKitPlayer: Applying LIVE optimization profile (STABILITY)',
);
(_player.platform as dynamic)?.setProperty(
'cache-secs', '10'); // Reduced to 10s for faster startup
'cache-secs',
'10',
); // Reduced to 10s for faster startup
(_player.platform as dynamic)?.setProperty(
'demuxer-max-bytes',
'32000000',
@@ -190,7 +192,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
final backBytes = (bufferBytes / 10).toInt(); // 10% for backward buffer
debugPrint(
'MediaKitPlayer: VOD profile - ${deviceInfo.getDeviceProfile()}, buffer=${bufferBytes ~/ (1024 * 1024)}MB');
'MediaKitPlayer: VOD profile - ${deviceInfo.getDeviceProfile()}, buffer=${bufferBytes ~/ (1024 * 1024)}MB',
);
(_player.platform as dynamic)?.setProperty('cache-secs', '100');
(_player.platform as dynamic)
@@ -309,7 +312,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_checkAndMarkWatched(position);
// Auto-save position every 10 seconds (resilience against app kill/crash)
if (widget.streamType != model.StreamType.live && !_hasMarkedAsWatched) {
if (widget.streamType != model.StreamType.live &&
!_hasMarkedAsWatched) {
final now = DateTime.now();
if (_lastSaveTime == null ||
now.difference(_lastSaveTime!) > const Duration(seconds: 10)) {
@@ -594,7 +598,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
Future.delayed(Duration(seconds: 2 * _reconnectAttempts), () {
if (mounted) {
// If VOD/Series, try to resume from current position
if (widget.streamType != model.StreamType.live && _position.inSeconds > 0) {
if (widget.streamType != model.StreamType.live &&
_position.inSeconds > 0) {
_loadStream(startAt: _position);
} else {
_loadStream();
@@ -949,7 +954,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Channel Down = Previous Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelDown) {
if (widget.streamType == model.StreamType.live && widget.channels != null) {
if (widget.streamType == model.StreamType.live &&
widget.channels != null) {
_playPrevious();
_onUserInteraction();
return true;
@@ -958,7 +964,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Channel Up = Next Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelUp) {
if (widget.streamType == model.StreamType.live && widget.channels != null) {
if (widget.streamType == model.StreamType.live &&
widget.channels != null) {
_playNext();
_onUserInteraction();
return true;
@@ -1522,9 +1529,11 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
LogicalKeyboardKey.arrowLeft) {
final newPos =
_position - const Duration(seconds: 10);
_player.seek(newPos < Duration.zero
? Duration.zero
: newPos);
_player.seek(
newPos < Duration.zero
? Duration.zero
: newPos,
);
_resetControlsTimer();
return KeyEventResult.handled;
}