Fix: Movie infinite scroll + Player controls auto-hide

- Added in-memory caching for movies/series pagination in XtreamServiceMobile
- Fixed player controls appearing then instantly disappearing on touch
- Updated refresh methods to clear cache on pull-to-refresh
- Added releases/ to gitignore (APKs too large for GitHub)
This commit is contained in:
Michael committed 2025-12-10 10:23:04 +01:00
1 parent 3888a21990
commit ccda497236
8 files changed
+330 -83

No files matched your search

+2
View File
@@ -49,3 +49,5 @@ app.*.map.json
# Hive data (local development)
*.hive
*.lock
releases/
@@ -17,6 +17,12 @@ class XtreamServiceMobile {
String? _resolvedIp; // Cached resolved IP for the playlist server
String? _originalHost; // Original hostname for Host header
// In-memory cache for pagination performance
List<dynamic>? _cachedMoviesRaw;
List<dynamic>? _cachedSeriesRaw;
Map<String, String>? _cachedVodCategories;
Map<String, String>? _cachedSeriesCategories;
XtreamServiceMobile(String cachePath) {
_dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 15),
@@ -282,25 +288,31 @@ class XtreamServiceMobile {
}
}
/// Get movies with pagination support
/// Get movies with pagination support (uses in-memory cache for performance)
Future<List<xm.Movie>> getMoviesPaginated({int offset = 0, int limit = 100}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
// Load categories for mapping
final categoryMap = await _getVodCategories();
// Load from cache or fetch from API
if (_cachedMoviesRaw == null) {
// First load: fetch from API and cache
_cachedVodCategories ??= await _getVodCategories();
final response = await _dio.get(
_effectiveApiBaseUrl,
queryParameters: {
'username': _currentPlaylist!.username,
'password': _currentPlaylist!.password,
'action': 'get_vod_streams',
},
options: _getOptions(),
);
final response = await _dio.get(
_effectiveApiBaseUrl,
queryParameters: {
'username': _currentPlaylist!.username,
'password': _currentPlaylist!.password,
'action': 'get_vod_streams',
},
options: _getOptions(),
);
final List<dynamic> allMovies = response.data as List<dynamic>;
_cachedMoviesRaw = response.data as List<dynamic>;
}
final allMovies = _cachedMoviesRaw!;
final categoryMap = _cachedVodCategories!;
// Apply pagination
final endIndex = (offset + limit) > allMovies.length ? allMovies.length : offset + limit;
@@ -309,7 +321,7 @@ class XtreamServiceMobile {
final paginatedMovies = allMovies.sublist(offset, endIndex);
return paginatedMovies.map((movieData) {
final data = movieData as Map<String, dynamic>;
final data = Map<String, dynamic>.from(movieData as Map<String, dynamic>);
final categoryId = data['category_id']?.toString() ?? '';
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
return xm.Movie.fromJson(data);
@@ -359,25 +371,31 @@ class XtreamServiceMobile {
}
}
/// Get series with pagination support (returns flat list)
/// Get series with pagination support (uses in-memory cache for performance)
Future<List<xm.Series>> getSeriesPaginated({int offset = 0, int limit = 100}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
// Load categories for mapping
final categoryMap = await _getSeriesCategories();
// Load from cache or fetch from API
if (_cachedSeriesRaw == null) {
// First load: fetch from API and cache
_cachedSeriesCategories ??= await _getSeriesCategories();
final response = await _dio.get(
_effectiveApiBaseUrl,
queryParameters: {
'username': _currentPlaylist!.username,
'password': _currentPlaylist!.password,
'action': 'get_series',
},
options: _getOptions(),
);
final response = await _dio.get(
_effectiveApiBaseUrl,
queryParameters: {
'username': _currentPlaylist!.username,
'password': _currentPlaylist!.password,
'action': 'get_series',
},
options: _getOptions(),
);
final List<dynamic> allSeries = response.data as List<dynamic>;
_cachedSeriesRaw = response.data as List<dynamic>;
}
final allSeries = _cachedSeriesRaw!;
final categoryMap = _cachedSeriesCategories!;
// Apply pagination
final endIndex = (offset + limit) > allSeries.length ? allSeries.length : offset + limit;
@@ -386,7 +404,7 @@ class XtreamServiceMobile {
final paginatedSeries = allSeries.sublist(offset, endIndex);
return paginatedSeries.map((seriesData) {
final data = seriesData as Map<String, dynamic>;
final data = Map<String, dynamic>.from(seriesData as Map<String, dynamic>);
final categoryId = data['category_id']?.toString() ?? '';
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
return xm.Series.fromJson(data);
@@ -530,8 +548,17 @@ class XtreamServiceMobile {
}
}
/// Clear in-memory cache (call on refresh)
void clearCache() {
_cachedMoviesRaw = null;
_cachedSeriesRaw = null;
_cachedVodCategories = null;
_cachedSeriesCategories = null;
}
/// Dispose resources
void dispose() {
clearCache();
_dio.close();
}
}
@@ -55,10 +55,28 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
bool _isSeeking = false;
bool _useSoftwareDecoder = false;
Timer? _clockTimer;
Timer? _controlsTimer; // Auto-hide timer
String _currentTime = '';
void _resetControlsTimer() {
_controlsTimer?.cancel();
if (_showControls && _isPlaying) {
_controlsTimer = Timer(const Duration(seconds: 4), () {
if (mounted) {
setState(() => _showControls = false);
}
});
}
}
void _onUserInteraction() {
setState(() => _showControls = true);
_resetControlsTimer();
}
@override
void initState() {
super.initState();
@@ -79,12 +97,40 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
// Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps)
(_player.platform as dynamic)?.setProperty('hwdec', 'auto');
// Reverted to Basic Configuration (Stock MPV) for maximum stability
// The previous advanced caching was causing streams to cut off after ~30s.
// We only enable basic cache and let MPV handle the rest automatically.
// Reliable Network Configuration (Fix for 30s cutoff)
// - Increase timeout significantly
// - Allow large cache duration (not just bytes)
// - Disable SSL verification to prevent handshake drops
// - Enable reconnect
// Reliable Network Configuration (Fix for 30s cutoff)
(_player.platform as dynamic)?.setProperty('cache', 'yes');
(_player.platform as dynamic)?.setProperty('cache-secs', '120'); // Cache up to 2 minutes
(_player.platform as dynamic)?.setProperty('demuxer-max-bytes', '50000000'); // 50MB max buffer
(_player.platform as dynamic)?.setProperty('network-timeout', '120'); // 2 minutes timeout
(_player.platform as dynamic)?.setProperty('tls-verify', 'no'); // Ignore SSL errors
(_player.platform as dynamic)?.setProperty('http-header-fields', 'User-Agent: XtremFlow/1.0');
_controller = VideoController(_player);
// Listen to player state
_player.stream.playing.listen((playing) {
if (mounted) {
setState(() => _isPlaying = playing);
if (playing) {
// Smooth Loading: Hide loading screen 1s AFTER playback actually starts
// This masks initial glitches/re-buffering
Future.delayed(const Duration(seconds: 1), () {
if (mounted && _isPlaying) {
setState(() => _isLoading = false);
}
});
_resetControlsTimer(); // Start auto-hide timer
}
}
});
@@ -100,13 +146,31 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
}
});
_player.stream.error.listen((error) {
if (mounted && error.isNotEmpty) {
setState(() {
_errorMessage = error;
_isLoading = false;
});
_player.stream.error.listen((error) async {
if (!mounted) return;
// Filter out non-errors or non-fatal messages if necessary
if (error.isEmpty) return;
debugPrint('MediaKitPlayer Error Stream: $error');
// Smart Retry for Codec/Decoder errors
// If we haven't tried SW decoding yet, retry with it enabled.
if (!_useSoftwareDecoder) {
debugPrint('MediaKitPlayer: Error encountered. Auto-retrying with Software Decoding...');
setState(() => _useSoftwareDecoder = true);
// Short delay to allow player cleanup
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) _loadStream(); // Retry connection
return;
}
// If SW decoding was already on or failed too, show error
setState(() {
_errorMessage = error;
_isLoading = false;
});
});
// Lock to landscape for video playback
@@ -137,6 +201,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
@override
void dispose() {
_clockTimer?.cancel();
_controlsTimer?.cancel();
_player.dispose();
_xtreamService?.dispose();
@@ -211,6 +276,10 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
debugPrint('MediaKitPlayer: Loading stream: $streamUrl');
// Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps)
// If we are in fallback mode, force software ('no')
(_player.platform as dynamic)?.setProperty('hwdec', _useSoftwareDecoder ? 'no' : 'auto');
await _player.open(
Media(streamUrl, httpHeaders: {'User-Agent': 'XtremFlow/1.0'}),
play: true,
@@ -218,9 +287,24 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
setState(() {
_isLoading = false;
// Reset fallback flag on success? No, keep it if it works to avoid future crashes on same stream
});
} catch (e) {
debugPrint('MediaKitPlayer error: $e');
// Automatic Retry with Software Decoding if it fails the first time
if (!_useSoftwareDecoder) {
debugPrint('MediaKitPlayer: Retrying with Software Decoding forced...');
setState(() {
_useSoftwareDecoder = true;
// _isLoading remains true
});
// Delay slightly to let player reset state if needed
await Future.delayed(const Duration(milliseconds: 500));
_loadStream(); // Recursive retry
return;
}
setState(() {
_isLoading = false;
_errorMessage = e.toString();
@@ -264,12 +348,15 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
} else {
_player.play();
}
_onUserInteraction();
}
void _toggleControls() {
setState(() {
_showControls = !_showControls;
});
// Always show controls on tap, then start auto-hide timer
if (!_showControls) {
setState(() => _showControls = true);
}
_resetControlsTimer();
}
@@ -286,14 +373,15 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _toggleControls,
onPanDown: (_) => _onUserInteraction(),
onPanUpdate: (_) => _onUserInteraction(),
child: Stack(
children: [
// Video Player
if (_errorMessage != null)
_buildErrorView()
else if (_isLoading)
_buildLoadingView()
else
Center(
child: Video(
@@ -302,6 +390,14 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
),
),
// Loading Overlay (Stays on top until smooth playback starts)
// We moved _buildLoadingView out of the if/else chain above to overlay it
if (_isLoading && _errorMessage == null)
Container(
color: Colors.black,
child: _buildLoadingView(),
),
// Custom controls overlay
if (_showControls && _errorMessage == null && !_isLoading)
_buildControlsOverlay(title),
@@ -637,7 +733,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
foregroundColor: Colors.black,
),
),
],
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'dart:io';
import '../../../providers/mobile_xtream_providers.dart';
import '../../../providers/mobile_settings_providers.dart';
@@ -19,7 +20,7 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
ConsumerState<MobileLiveTVTab> createState() => _MobileLiveTVTabState();
}
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with AutomaticKeepAliveClientMixin {
String? _selectedCategory;
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@@ -43,8 +44,12 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
bool _isCategoryView = true;
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
final channelsAsync = ref.watch(mobileLiveChannelsProvider(widget.playlist));
final favorites = ref.watch(mobileFavoritesProvider);
final settings = ref.watch(mobileSettingsProvider);
@@ -214,7 +219,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8, // Drastically reduced size as requested
crossAxisCount: 6, // Adjusted to 6 columns as requested
childAspectRatio: 1.0,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
@@ -386,10 +391,11 @@ class _MobileChannelTileState extends ConsumerState<_MobileChannelTile> {
borderRadius: BorderRadius.circular(8),
),
child: iconUrl != null
? Image.network(
iconUrl,
? CachedNetworkImage(
imageUrl: iconUrl,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(Icons.tv, color: Colors.white24),
errorWidget: (_, __, ___) => const Icon(Icons.tv, color: Colors.white24),
placeholder: (_, __) => const Icon(Icons.tv, color: Colors.white24),
)
: const Icon(Icons.tv, color: Colors.white24),
),
@@ -18,7 +18,7 @@ class MobileMoviesTab extends ConsumerStatefulWidget {
ConsumerState<MobileMoviesTab> createState() => _MobileMoviesTabState();
}
class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> with AutomaticKeepAliveClientMixin {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
final List<Movie> _movies = [];
@@ -28,7 +28,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
bool _isSearching = false;
bool _hasMore = true;
int _currentOffset = 0;
static const int _pageSize = 500; // Large page size to prevent infinite scroll bugs if offset logic is flawed
static const int _pageSize = 50;
Timer? _searchDebounce;
@override
@@ -99,36 +99,51 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
try {
final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
// Add timeout to prevent infinite loading state
final newMovies = await service.getMoviesPaginated(
offset: _currentOffset,
limit: _pageSize,
);
).timeout(const Duration(seconds: 15));
if (mounted) {
setState(() {
if (newMovies.isEmpty) {
_hasMore = false;
} else {
_movies.addAll(newMovies);
_currentOffset += _pageSize;
// If we got fewer movies than requested, we reached the end
if (newMovies.length < _pageSize) {
_hasMore = false;
}
}
_movies.addAll(newMovies);
_currentOffset += _pageSize;
_hasMore = newMovies.length == _pageSize;
_isLoading = false;
});
}
} catch (e) {
debugPrint('Error loading movies: $e');
if (mounted) {
setState(() {
_isLoading = false;
_hasMore = false; // Stop trying on error to prevent infinite loop
});
setState(() {
_isLoading = false;
// Do NOT set _hasMore to false on error
});
}
}
}
Future<void> _refresh() async {
// Clear service cache to force fresh data
try {
final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
service.clearCache();
} catch (_) {}
setState(() {
_movies.clear();
_currentOffset = 0;
_hasMore = true;
_searchResults = null;
_searchQuery = '';
_searchController.clear();
_isLoading = false;
});
await _loadMoreMovies();
}
String? _formatRating(String? rating) {
if (rating == null || rating.isEmpty) return null;
final value = double.tryParse(rating);
@@ -161,8 +176,12 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
);
}
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
final settings = ref.watch(mobileSettingsProvider);
final watchHistory = ref.watch(mobileWatchHistoryProvider);
@@ -186,8 +205,13 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
return SafeArea(
bottom: false,
child: CustomScrollView(
child: RefreshIndicator(
onRefresh: _refresh,
color: AppColors.primary,
backgroundColor: AppColors.surface,
child: CustomScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// Header & Search
SliverToBoxAdapter(
@@ -234,18 +258,22 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
),
),
// Hero Section (Only if not searching)
// Hero Section
if (_searchQuery.isEmpty && heroItems.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
height: 250, // Reduced height for mobile hero
height: 250,
child: HeroCarousel(
items: heroItems,
onTap: (item) {
final movie = _movies.firstWhere((element) => element.streamId == item.id);
_playMovie(movie);
try {
final movie = _movies.firstWhere((element) => element.streamId == item.id);
_playMovie(movie);
} catch (e) {
// Fallback ignored
}
},
),
),
@@ -253,18 +281,17 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
),
// Grid
if (displayMovies.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8,
childAspectRatio: 0.7,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.7,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= displayMovies.length) return null;
final movie = displayMovies[index];
final isWatched = watchHistory.isMovieWatched(movie.streamId);
@@ -296,6 +323,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
),
],
),
),
);
}
}
@@ -18,7 +18,7 @@ class MobileSeriesTab extends ConsumerStatefulWidget {
ConsumerState<MobileSeriesTab> createState() => _MobileSeriesTabState();
}
class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> with AutomaticKeepAliveClientMixin {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
final List<Series> _series = [];
@@ -99,10 +99,12 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
try {
final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
// Add timeout to prevent infinite loading state
final newSeries = await service.getSeriesPaginated(
offset: _currentOffset,
limit: _pageSize,
);
).timeout(const Duration(seconds: 15));
if (mounted) {
setState(() {
@@ -113,12 +115,35 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
});
}
} catch (e) {
debugPrint('Error loading series: $e');
if (mounted) {
setState(() => _isLoading = false);
setState(() {
_isLoading = false;
// Do NOT set _hasMore to false on error
});
}
}
}
Future<void> _refresh() async {
// Clear service cache to force fresh data
try {
final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
service.clearCache();
} catch (_) {}
setState(() {
_series.clear();
_currentOffset = 0;
_hasMore = true;
_searchResults = null;
_searchQuery = '';
_searchController.clear();
_isLoading = false;
});
await _loadMoreSeries();
}
String? _formatRating(String? rating) {
if (rating == null || rating.isEmpty) return null;
final value = double.tryParse(rating);
@@ -146,8 +171,12 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
);
}
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
final settings = ref.watch(mobileSettingsProvider);
List<Series> displaySeries;
@@ -170,8 +199,13 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
return SafeArea(
bottom: false,
child: CustomScrollView(
child: RefreshIndicator(
onRefresh: _refresh,
color: AppColors.primary,
backgroundColor: AppColors.surface,
child: CustomScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// Header & Search
SliverToBoxAdapter(
@@ -283,6 +317,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
),
],
),
),
);
}
}
@@ -157,6 +157,48 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
),
const SizedBox(height: 24),
// Playback Settings
const _SectionHeader(title: 'Lecture Vidéo'),
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.speed, color: AppColors.primary),
title: const Text('Mise en mémoire tampon (Cache)',
style: TextStyle(color: AppColors.textPrimary)),
subtitle: Text(
settings.bufferDuration == 0
? 'Auto (Défaut)'
: '${settings.bufferDuration} secondes',
style: const TextStyle(color: AppColors.textSecondary),
),
trailing: DropdownButton<int>(
value: settings.bufferDuration,
dropdownColor: AppColors.surface,
underline: const SizedBox(),
icon: const Icon(Icons.arrow_drop_down, color: AppColors.primary),
items: const [
DropdownMenuItem(value: 0, child: Text('Auto', style: TextStyle(color: AppColors.textPrimary))),
DropdownMenuItem(value: 15, child: Text('15s', style: TextStyle(color: AppColors.textPrimary))),
DropdownMenuItem(value: 30, child: Text('30s', style: TextStyle(color: AppColors.textPrimary))),
DropdownMenuItem(value: 60, child: Text('60s', style: TextStyle(color: AppColors.textPrimary))),
],
onChanged: (val) {
if (val != null) {
ref.read(mobileSettingsProvider.notifier).setBufferDuration(val);
}
},
),
),
],
),
),
const SizedBox(height: 24),
// Content Filters
const _SectionHeader(title: 'Filtres de Contenu'),
Container(
@@ -8,12 +8,14 @@ class MobileSettings {
final List<String> moviesKeywords;
final List<String> seriesKeywords;
final bool showClock;
final int bufferDuration; // seconds, 0 = auto
const MobileSettings({
this.liveTvKeywords = const [],
this.moviesKeywords = const [],
this.seriesKeywords = const [],
this.showClock = false,
this.bufferDuration = 0,
});
MobileSettings copyWith({
@@ -21,12 +23,14 @@ class MobileSettings {
List<String>? moviesKeywords,
List<String>? seriesKeywords,
bool? showClock,
int? bufferDuration,
}) {
return MobileSettings(
liveTvKeywords: liveTvKeywords ?? this.liveTvKeywords,
moviesKeywords: moviesKeywords ?? this.moviesKeywords,
seriesKeywords: seriesKeywords ?? this.seriesKeywords,
showClock: showClock ?? this.showClock,
bufferDuration: bufferDuration ?? this.bufferDuration,
);
}
@@ -73,12 +77,14 @@ class MobileSettingsNotifier extends StateNotifier<MobileSettings> {
final movies = _box?.get('moviesKeywords', defaultValue: <String>[]) as List?;
final series = _box?.get('seriesKeywords', defaultValue: <String>[]) as List?;
final showClock = _box?.get('showClock', defaultValue: false) as bool?;
final buffer = _box?.get('bufferDuration', defaultValue: 0) as int?;
state = MobileSettings(
liveTvKeywords: liveTv?.cast<String>() ?? [],
moviesKeywords: movies?.cast<String>() ?? [],
seriesKeywords: series?.cast<String>() ?? [],
showClock: showClock ?? false,
bufferDuration: buffer ?? 0,
);
}
@@ -101,6 +107,11 @@ class MobileSettingsNotifier extends StateNotifier<MobileSettings> {
state = state.copyWith(showClock: value);
_box?.put('showClock', value);
}
void setBufferDuration(int seconds) {
state = state.copyWith(bufferDuration: seconds);
_box?.put('bufferDuration', seconds);
}
}
final mobileSettingsProvider = StateNotifierProvider<MobileSettingsNotifier, MobileSettings>((ref) {