mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
Feature: Resume playback from last position
- Added resumePositions storage in MobileWatchHistory - Save position on player exit (if not finished) - Auto-seek to saved position on content load - Clear resume position when content finished (80%+)
This commit is contained in:
1 parent
d196b1dce0
commit
bf5893e545
2 files changed
+86
-2
No files matched your search
@@ -228,6 +228,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Save resume position before cleanup (only for VOD/Series)
|
||||
_saveResumePositionOnExit();
|
||||
|
||||
_clockTimer?.cancel();
|
||||
_controlsTimer?.cancel();
|
||||
_liveWatchdog?.cancel();
|
||||
@@ -246,6 +249,38 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Get content ID for resume position storage
|
||||
String get _contentId {
|
||||
if (widget.streamType == StreamType.series &&
|
||||
widget.seriesId != null &&
|
||||
widget.season != null &&
|
||||
widget.episodeNum != null) {
|
||||
return MobileWatchHistory.episodeKey(widget.seriesId, widget.season!, widget.episodeNum!);
|
||||
}
|
||||
return widget.streamId;
|
||||
}
|
||||
|
||||
/// Save resume position when exiting player
|
||||
void _saveResumePositionOnExit() {
|
||||
if (widget.streamType == StreamType.live) return;
|
||||
if (_duration.inSeconds <= 0) return;
|
||||
|
||||
final progress = _position.inSeconds / _duration.inSeconds;
|
||||
|
||||
// Don't save if almost finished (> 90%) or already marked as watched
|
||||
if (progress > 0.90 || _hasMarkedAsWatched) {
|
||||
ref.read(mobileWatchHistoryProvider.notifier).clearResumePosition(_contentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save position
|
||||
ref.read(mobileWatchHistoryProvider.notifier).saveResumePosition(
|
||||
_contentId,
|
||||
_position.inSeconds,
|
||||
);
|
||||
debugPrint('MediaKitPlayer: Saved resume position at ${_position.inSeconds}s');
|
||||
}
|
||||
|
||||
/// Attempt to reconnect a live stream that stopped
|
||||
void _attemptReconnect() {
|
||||
if (!mounted) return;
|
||||
@@ -393,6 +428,20 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
if (widget.streamType == StreamType.live) {
|
||||
_startLiveWatchdog();
|
||||
}
|
||||
|
||||
// Resume from saved position (VOD/Series only)
|
||||
if (widget.streamType != StreamType.live) {
|
||||
final resumePos = ref.read(mobileWatchHistoryProvider).getResumePosition(_contentId);
|
||||
if (resumePos > 30) {
|
||||
// Wait for player to stabilize, then seek
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted && _isPlaying) {
|
||||
debugPrint('MediaKitPlayer: Resuming from ${resumePos}s');
|
||||
_player.seek(Duration(seconds: resumePos));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('MediaKitPlayer error: $e');
|
||||
|
||||
|
||||
@@ -122,25 +122,32 @@ final mobileSettingsProvider = StateNotifierProvider<MobileSettingsNotifier, Mob
|
||||
class MobileWatchHistory {
|
||||
final Set<String> watchedMovies;
|
||||
final Set<String> watchedEpisodes;
|
||||
final Map<String, int> resumePositions; // streamId -> position in seconds
|
||||
|
||||
const MobileWatchHistory({
|
||||
this.watchedMovies = const {},
|
||||
this.watchedEpisodes = const {},
|
||||
this.resumePositions = const {},
|
||||
});
|
||||
|
||||
MobileWatchHistory copyWith({
|
||||
Set<String>? watchedMovies,
|
||||
Set<String>? watchedEpisodes,
|
||||
Map<String, int>? resumePositions,
|
||||
}) {
|
||||
return MobileWatchHistory(
|
||||
watchedMovies: watchedMovies ?? this.watchedMovies,
|
||||
watchedEpisodes: watchedEpisodes ?? this.watchedEpisodes,
|
||||
resumePositions: resumePositions ?? this.resumePositions,
|
||||
);
|
||||
}
|
||||
|
||||
bool isMovieWatched(String streamId) => watchedMovies.contains(streamId);
|
||||
bool isEpisodeWatched(String episodeKey) => watchedEpisodes.contains(episodeKey);
|
||||
|
||||
/// Get resume position in seconds (0 if none saved)
|
||||
int getResumePosition(String contentId) => resumePositions[contentId] ?? 0;
|
||||
|
||||
static String episodeKey(dynamic seriesId, int season, int episodeNum) {
|
||||
return '$seriesId:$season:$episodeNum';
|
||||
}
|
||||
@@ -159,17 +166,23 @@ class MobileWatchHistoryNotifier extends StateNotifier<MobileWatchHistory> {
|
||||
|
||||
final movies = _box?.get('watchedMovies', defaultValue: <String>[]) as List?;
|
||||
final episodes = _box?.get('watchedEpisodes', defaultValue: <String>[]) as List?;
|
||||
final positions = _box?.get('resumePositions', defaultValue: <String, dynamic>{}) as Map?;
|
||||
|
||||
state = MobileWatchHistory(
|
||||
watchedMovies: movies?.cast<String>().toSet() ?? {},
|
||||
watchedEpisodes: episodes?.cast<String>().toSet() ?? {},
|
||||
resumePositions: positions?.cast<String, int>() ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
void markMovieWatched(String streamId) {
|
||||
final updated = {...state.watchedMovies, streamId};
|
||||
state = state.copyWith(watchedMovies: updated);
|
||||
// Clear resume position when marked as fully watched
|
||||
final positions = {...state.resumePositions};
|
||||
positions.remove(streamId);
|
||||
state = state.copyWith(watchedMovies: updated, resumePositions: positions);
|
||||
_box?.put('watchedMovies', updated.toList());
|
||||
_box?.put('resumePositions', positions);
|
||||
}
|
||||
|
||||
void toggleMovieWatched(String streamId) {
|
||||
@@ -185,8 +198,30 @@ class MobileWatchHistoryNotifier extends StateNotifier<MobileWatchHistory> {
|
||||
|
||||
void markEpisodeWatched(String episodeKey) {
|
||||
final updated = {...state.watchedEpisodes, episodeKey};
|
||||
state = state.copyWith(watchedEpisodes: updated);
|
||||
// Clear resume position when marked as fully watched
|
||||
final positions = {...state.resumePositions};
|
||||
positions.remove(episodeKey);
|
||||
state = state.copyWith(watchedEpisodes: updated, resumePositions: positions);
|
||||
_box?.put('watchedEpisodes', updated.toList());
|
||||
_box?.put('resumePositions', positions);
|
||||
}
|
||||
|
||||
/// Save resume position for movie or episode
|
||||
void saveResumePosition(String contentId, int positionSeconds) {
|
||||
// Only save if position is meaningful (> 30 seconds and < 95% of typical content)
|
||||
if (positionSeconds < 30) return;
|
||||
|
||||
final positions = {...state.resumePositions, contentId: positionSeconds};
|
||||
state = state.copyWith(resumePositions: positions);
|
||||
_box?.put('resumePositions', positions);
|
||||
}
|
||||
|
||||
/// Clear resume position (when content finished or user wants to restart)
|
||||
void clearResumePosition(String contentId) {
|
||||
final positions = {...state.resumePositions};
|
||||
positions.remove(contentId);
|
||||
state = state.copyWith(resumePositions: positions);
|
||||
_box?.put('resumePositions', positions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user