feat: add wakelock_plus dependency and update task list to focus on Fire Stick fixes.

This commit is contained in:
Michael committed 2025-12-11 19:48:44 +01:00
1 parent 8916d6f1ee
commit 43921abd2e
6 files changed
+170 -148

No files matched your search

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:video_player/video_player.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
import 'package:path_provider/path_provider.dart';
import 'package:xtremflow/core/models/iptv_models.dart';
@@ -82,6 +83,9 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen> with Widget
WidgetsBinding.instance.addObserver(this);
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
// Enable wakelock to prevent screen from turning off during playback
WakelockPlus.enable();
_currentIndex = widget.initialIndex;
_startClock();
@@ -350,6 +354,10 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen> with Widget
void dispose() {
WidgetsBinding.instance.removeObserver(this);
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
// Disable wakelock when leaving player
WakelockPlus.disable();
_controlsTimer?.cancel();
_clockTimer?.cancel();
_epgTimer?.cancel();
@@ -445,39 +453,58 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen> with Widget
),
),
// Center Controls
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Replay 10s button only for VOD
if (widget.streamType != StreamType.live) ...[
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white, size: 40),
onPressed: () => _seek(const Duration(seconds: -10)),
),
const SizedBox(width: 32),
],
TVFocusable(
focusNode: _playPauseFocusNode,
onPressed: _togglePlayPause,
child: Icon(
_isPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled,
color: AppColors.primary,
size: 64,
),
// Center Controls - Positioned lower with transparent background
Positioned(
bottom: 100, // Lower position (above progress bar area)
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.4),
borderRadius: BorderRadius.circular(40),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Replay 10s button only for VOD
if (widget.streamType != StreamType.live) ...[
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white70, size: 36),
onPressed: () => _seek(const Duration(seconds: -10)),
),
const SizedBox(width: 24),
],
TVFocusable(
focusNode: _playPauseFocusNode,
onPressed: _togglePlayPause,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
shape: BoxShape.circle,
),
child: Icon(
_isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
color: Colors.white,
size: 48,
),
),
),
// Forward 10s button only for VOD
if (widget.streamType != StreamType.live) ...[
const SizedBox(width: 32),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white, size: 40),
onPressed: () => _seek(const Duration(seconds: 10)),
),
],
],
// Forward 10s button only for VOD
if (widget.streamType != StreamType.live) ...[
const SizedBox(width: 24),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white70, size: 36),
onPressed: () => _seek(const Duration(seconds: 10)),
),
],
],
),
),
),
),
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
import 'package:path_provider/path_provider.dart';
import 'package:xtremflow/core/models/iptv_models.dart';
@@ -110,6 +111,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> with Wi
// Register keyboard handler for remote control
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
// Enable wakelock to prevent screen from turning off during playback
WakelockPlus.enable();
// Only start clock if setting is enabled (will check in build too, but timer can run)
_startClock();
_currentIndex = widget.initialIndex;
@@ -277,6 +281,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> with Wi
// Unregister keyboard handler
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
// Disable wakelock when leaving player
WakelockPlus.disable();
// Save resume position before cleanup (only for VOD/Series)
_saveResumePositionOnExit();
@@ -25,12 +25,12 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with AutomaticKeepAliveClientMixin {
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
String _searchQuery = '';
bool _showFavoritesOnly = false;
bool _isSearchEditing = false;
bool _justReturnedFromPlayer = false; // Flag to prevent PopScope interception after player
@override
void initState() {
@@ -49,9 +49,6 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
super.dispose();
}
@override
bool get wantKeepAlive => true;
@@ -69,10 +66,17 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
child: Scaffold(
backgroundColor: Colors.transparent,
body: PopScope(
// Only allow full pop if we're at category view with no filters AND not just returned from player
canPop: uiState.isCategoryView && _searchQuery.isEmpty && !_showFavoritesOnly,
onPopInvoked: (didPop) {
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
// If just returned from player, reset the flag and do nothing (stay on channel list)
if (_justReturnedFromPlayer) {
_justReturnedFromPlayer = false;
return;
}
if (_searchQuery.isNotEmpty) {
setState(() {
_searchController.clear();
@@ -354,38 +358,43 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
);
}
void _playChannel(BuildContext context, Channel channel, List<Channel> channels, PlaylistConfig playlist, int index) {
final engine = ref.read(mobileSettingsProvider).playerEngine;
if (engine == 'lite') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LitePlayerScreen(
streamId: channel.streamId,
title: channel.name,
playlist: playlist,
streamType: StreamType.live,
channels: channels, // Pass full channel list for zapping
initialIndex: index,
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NativePlayerScreen(
streamId: channel.streamId,
title: channel.name,
playlist: playlist,
streamType: StreamType.live,
channels: channels, // Pass full channel list for zapping
initialIndex: index,
),
),
);
}
void _playChannel(BuildContext context, Channel channel, List<Channel> channels, PlaylistConfig playlist, int index) async {
final engine = ref.read(mobileSettingsProvider).playerEngine;
if (engine == 'lite') {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LitePlayerScreen(
streamId: channel.streamId,
title: channel.name,
playlist: playlist,
streamType: StreamType.live,
channels: channels, // Pass full channel list for zapping
initialIndex: index,
),
),
);
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NativePlayerScreen(
streamId: channel.streamId,
title: channel.name,
playlist: playlist,
streamType: StreamType.live,
channels: channels, // Pass full channel list for zapping
initialIndex: index,
),
),
);
}
// When returning from player, set flag to prevent PopScope from going back to categories
if (mounted) {
setState(() => _justReturnedFromPlayer = true);
}
}
}
@@ -130,35 +130,47 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
// Appearance
const _SectionHeader(title: 'Apparence'),
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: SwitchListTile(
title: const Text('Mode Sombre',
style: TextStyle(color: AppColors.textPrimary)),
secondary: Icon(isDark ? Icons.dark_mode : Icons.light_mode,
color: AppColors.primary),
value: isDark,
onChanged: (_) => themeNotifier.toggleTheme(),
activeColor: AppColors.primary,
// Dark Mode
TVFocusable(
onPressed: () => themeNotifier.toggleTheme(),
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: SwitchListTile(
title: const Text('Mode Sombre',
style: TextStyle(color: AppColors.textPrimary)),
secondary: Icon(isDark ? Icons.dark_mode : Icons.light_mode,
color: AppColors.primary),
value: isDark,
onChanged: (_) => themeNotifier.toggleTheme(),
activeColor: AppColors.primary,
),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: SwitchListTile(
title: const Text('Afficher l\'heure',
style: TextStyle(color: AppColors.textPrimary)),
secondary: const Icon(Icons.access_time,
color: AppColors.primary),
value: settings.showClock,
onChanged: (val) => ref.read(mobileSettingsProvider.notifier).toggleShowClock(val),
activeColor: AppColors.primary,
// Show Clock
TVFocusable(
onPressed: () => ref.read(mobileSettingsProvider.notifier).toggleShowClock(!settings.showClock),
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: SwitchListTile(
title: const Text('Afficher l\'heure',
style: TextStyle(color: AppColors.textPrimary)),
secondary: const Icon(Icons.access_time,
color: AppColors.primary),
value: settings.showClock,
onChanged: (val) => ref.read(mobileSettingsProvider.notifier).toggleShowClock(val),
activeColor: AppColors.primary,
),
),
),
const SizedBox(height: 24),
@@ -364,8 +376,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
// Refresh Cache Section
const _SectionHeader(title: 'Cache'),
InkWell(
onTap: _isRefreshingCache ? null : () async {
TVFocusable(
onPressed: _isRefreshingCache ? null : () async {
setState(() => _isRefreshingCache = true);
try {
@@ -442,8 +454,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
const SizedBox(height: 16),
// Change Playlist
InkWell(
onTap: () {
TVFocusable(
onPressed: () {
// Open Playlist Selection in Manage Mode
Navigator.push(
context,