chore: Update Docker cache buster version.

This commit is contained in:
Michael committed 2025-12-13 10:37:46 +01:00
1 parent d0dda60f8c
commit 18d2e0a0ee
6 files changed
+408 -147

No files matched your search

+191 -145
View File
@@ -8,108 +8,31 @@ import 'package:pointer_interceptor/pointer_interceptor.dart';
import '../providers/xtream_provider.dart';
import '../providers/playback_positions_provider.dart';
import '../providers/settings_provider.dart';
import '../widgets/epg_overlay.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/models/iptv_models.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/glass_container.dart';
import '../../../core/widgets/tv_focusable_card.dart';
import '../widgets/lite_player_view.dart';
enum StreamType { live, vod, series }
class PlayerScreen extends ConsumerStatefulWidget {
final String streamId;
final String title;
final StreamType streamType;
final String containerExtension;
final PlaylistConfig playlist;
final List<Channel>? channels;
final int initialIndex;
const PlayerScreen({
super.key,
required this.streamId,
required this.title,
required this.playlist,
this.streamType = StreamType.live,
this.containerExtension = 'mp4',
this.channels,
this.initialIndex = 0,
});
@override
ConsumerState<PlayerScreen> createState() => _PlayerScreenState();
}
// ... (existing imports)
class _PlayerScreenState extends ConsumerState<PlayerScreen> {
// ... (Keep existing state variables)
String? _errorMessage;
String? _statusMessage;
late String _viewId;
late String _contentId;
late int _currentIndex;
bool _isInitialized = false;
bool _isLoading = true;
bool _showControls = false;
bool _isPlaying = true;
bool _showControls = false;
Timer? _controlsTimer;
double _currentPosition = 0;
double _totalDuration = 1;
double _volume = 1.0;
double _previousVolume = 1.0;
bool _isFullscreen = false;
Timer? _controlsTimer;
int _currentIndex = 0;
bool _isInitialized = false;
bool _isLoading = true;
StreamSubscription? _messageSubscription;
List<Map<String, dynamic>> _audioTracks = [];
String _aspectRatio = 'contain';
// ... (Keep existing methods: _formatDuration, _setupMessageListener, _sendMessage, etc.)
// For brevity in this artifact, I will focus on the BUILD method and styling changes.
// The logic remains largely the same, just the UI wrapper changes.
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
}
void _setupMessageListener() {
_messageSubscription = html.window.onMessage.listen((event) {
// ... (Same logic as before)
try {
final data = event.data;
if (data is Map) {
final type = data['type'];
if (type == 'playback_position' && _contentId.isNotEmpty) {
final currentTime = (data['currentTime'] as num).toDouble();
final rawDuration = (data['duration'] as num).toDouble();
final duration = rawDuration.isFinite ? rawDuration : 0.0;
setState(() {
_currentPosition = currentTime;
_totalDuration = duration > 0 ? duration : 1;
});
} else if (type == 'playback_status') {
setState(() => _isPlaying = data['status'] == 'playing');
} else if (type == 'user_activity') {
_onHover();
}
}
} catch (e) {
debugPrint('PostMessage Error: $e');
}
});
}
void _sendMessage(Map<String, dynamic> message) {
final iframe = html.document.getElementById(_viewId) as html.IFrameElement?;
iframe?.contentWindow?.postMessage(message, '*');
}
bool _isSeeking = false;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_initializePlayer();
if (widget.channels != null) {
_currentIndex = widget.channels!.indexWhere((c) => c.streamId == widget.streamId);
if (_currentIndex == -1) _currentIndex = 0;
}
_initializePlayer(startTimeOverride: widget.startTime);
_setupMessageListener();
}
@@ -120,53 +43,6 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
super.dispose();
}
void _onHover() {
if (!_showControls) setState(() => _showControls = true);
_controlsTimer?.cancel();
_controlsTimer = Timer(const Duration(seconds: 4), () {
if (mounted) setState(() => _showControls = false);
});
}
void _togglePlayPause() {
setState(() => _isPlaying = !_isPlaying);
_sendMessage({'type': _isPlaying ? 'play' : 'pause'});
_onHover();
}
void _toggleFullscreen() {
if (!_isFullscreen) {
html.document.documentElement?.requestFullscreen();
} else {
html.document.exitFullscreen();
}
setState(() => _isFullscreen = !_isFullscreen);
}
void _nextChannel() {
if (widget.channels == null || widget.channels!.isEmpty) return;
setState(() {
_currentIndex = (_currentIndex + 1) % widget.channels!.length;
_isInitialized = false; // Trigger reload
});
// Update player for new stream
// We need to re-initialize with the new stream ID from the list
_initializePlayer(isChannelSwitch: true);
}
void _previousChannel() {
if (widget.channels == null || widget.channels!.isEmpty) return;
setState(() {
_currentIndex = (_currentIndex - 1 + widget.channels!.length) % widget.channels!.length;
_isInitialized = false;
});
_initializePlayer(isChannelSwitch: true);
}
Future<void> _initializePlayer({double? startTimeOverride, bool isChannelSwitch = false}) async {
setState(() {
_isLoading = true;
@@ -193,6 +69,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
streamUrl = service.getSeriesStreamUrl(currentStreamId, widget.containerExtension);
}
// Store URL for Lite Player
_currentStreamUrl = streamUrl;
final encodedUrl = Uri.encodeComponent(streamUrl);
var playerSrc = 'player.html?url=$encodedUrl';
@@ -200,7 +79,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
playerSrc += '&t=$startTimeOverride';
}
// Register View Factory
// Register View Factory (Only needed for Standard Player, but safe to do always or conditionally)
ui_web.platformViewRegistry.registerViewFactory(_viewId, (int viewId) {
final iframe = html.IFrameElement()
..id = _viewId
@@ -228,8 +107,150 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
}
}
void _setupMessageListener() {
_messageSubscription = html.window.onMessage.listen((event) {
final data = event.data;
if (data == null) return;
final type = data['type'];
if (type == 'playback_position') {
final currentTime = (data['currentTime'] as num).toDouble();
final rawDuration = (data['duration'] as num).toDouble();
final duration = rawDuration.isFinite ? rawDuration : 0.0;
// Only update position if user is NOT dragging the slider
if (!_isSeeking) {
setState(() {
_currentPosition = currentTime;
_totalDuration = duration > 0 ? duration : 1;
});
}
// Update watch history if relevant
if (currentTime > 0) {
ref.read(playbackPositionsProvider.notifier).savePosition(
widget.playlist.id,
widget.streamId,
currentTime
);
}
} else if (type == 'playback_status') {
setState(() => _isPlaying = data['status'] == 'playing');
} else if (type == 'user_activity') {
_onHover();
}
});
}
void _sendMessage(Map<String, dynamic> message) {
if (_isInitialized) {
final iframe = html.document.getElementById(_viewId) as html.IFrameElement?;
iframe?.contentWindow?.postMessage(message, '*');
}
}
void _onHover() {
setState(() => _showControls = true);
_controlsTimer?.cancel();
_controlsTimer = Timer(const Duration(seconds: 4), () {
if (mounted && _isPlaying) setState(() => _showControls = false);
});
}
void _togglePlayPause() {
if (_isPlaying) {
_sendMessage({'type': 'pause'});
} else {
_sendMessage({'type': 'play'});
}
}
void _toggleFullscreen() {
if (html.document.fullscreenElement != null) {
html.document.exitFullscreen();
} else {
html.document.documentElement?.requestFullscreen();
}
}
void _previousChannel() {
if (widget.channels != null && widget.channels!.isNotEmpty) {
setState(() {
if (_currentIndex > 0) {
_currentIndex--;
} else {
_currentIndex = widget.channels!.length - 1;
}
});
_initializePlayer(isChannelSwitch: true);
}
}
void _nextChannel() {
if (widget.channels != null && widget.channels!.isNotEmpty) {
setState(() {
if (_currentIndex < widget.channels!.length - 1) {
_currentIndex++;
} else {
_currentIndex = 0;
}
});
_initializePlayer(isChannelSwitch: true);
}
}
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(d.inHours);
final minutes = twoDigits(d.inMinutes.remainder(60));
final seconds = twoDigits(d.inSeconds.remainder(60));
return d.inHours > 0 ? '$hours:$minutes:$seconds' : '$minutes:$seconds';
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(iptvSettingsProvider);
// LITE PLAYER MODE
if (settings.playerType == PlayerType.lite) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
if (_isInitialized && _currentStreamUrl != null)
LitePlayerView(
streamUrl: _currentStreamUrl!,
isLive: widget.streamType == StreamType.live,
onNext: widget.channels != null ? _nextChannel : null,
onPrevious: widget.channels != null ? _previousChannel : null,
)
else
const Center(child: CircularProgressIndicator(color: AppColors.primary)),
// Simple Back Button Overlay
Positioned(
top: 24,
left: 24,
child: TvFocusableCard(
onTap: () => Navigator.pop(context),
borderRadius: 50,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black45,
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_back_rounded, color: Colors.white),
),
),
),
],
),
);
}
// STANDARD PLAYER MODE (Custom HTML Overlay)
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
@@ -337,10 +358,10 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
),
// EPG Overlay (Moved out of controls box)
// EPG Overlay
if (widget.streamType == StreamType.live)
Positioned(
bottom: 140, // Positioned above the controls bar
bottom: 140,
left: 40,
child: EpgOverlay(
streamId: widget.channels != null
@@ -384,8 +405,19 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
value: _currentPosition,
min: 0,
max: _totalDuration,
onChanged: (v) {
_sendMessage({'type': 'seek', 'value': v});
onChangeStart: (value) {
setState(() => _isSeeking = true);
},
onChanged: (value) {
// Update UI immediately (optimistic update)
setState(() => _currentPosition = value);
},
onChangeEnd: (value) {
_sendMessage({'type': 'seek', 'value': value});
// Small delay to prevent jitter from incoming messages
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) setState(() => _isSeeking = false);
});
},
),
),
@@ -400,9 +432,23 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildTvControl(Icons.skip_previous_rounded, _previousChannel),
const SizedBox(width: 32), // Increased spacing
_buildTvControl(Icons.skip_next_rounded, _nextChannel),
// Channel Zapping for Live TV
if (widget.streamType == StreamType.live && widget.channels != null) ...[
_buildTvControl(Icons.navigate_before_rounded, _previousChannel),
const SizedBox(width: 32),
_buildTvControl(Icons.navigate_next_rounded, _nextChannel),
],
// Controls for VOD (Keep Skip buttons, they act as Prev/Next track if playlist or maybe seek)
// The user said: "only for live tv, other buttons skip/prev must allow changing channel"
// This implies for VOD they might want seek? But existing buttons were skip_previous/skip_next.
// We keep them for VOD if channels list is present, or just hide them if not needed.
// For now, I will restore them for VOD as well if channels > 0 (playlist mode)
if (widget.streamType != StreamType.live && widget.channels != null && widget.channels!.isNotEmpty) ...[
_buildTvControl(Icons.skip_previous_rounded, _previousChannel),
const SizedBox(width: 32),
_buildTvControl(Icons.skip_next_rounded, _nextChannel),
],
const Spacer(),
_buildTvControl(Icons.subtitles_rounded, () {}),