Skip to content

Commit 73356cf

Browse files
authored
Merge pull request #127 from avdept/autosave
Autosave position
2 parents 27d9500 + c4a1d52 commit 73356cf

5 files changed

Lines changed: 159 additions & 9 deletions

File tree

‎lib/src/app.dart‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'dart:async' show Timer;
1+
import 'dart:async' show Timer, unawaited;
22
import 'dart:io' show Platform;
33

44
import 'package:flutter/cupertino.dart';
@@ -201,6 +201,9 @@ class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
201201
})
202202
..listen(currentLibraryProvider, (previous, next) {
203203
_selectedLibrary = next.valueOrNull;
204+
if (next.valueOrNull != null) {
205+
unawaited(ref.read(playbackProvider.notifier).tryRestore());
206+
}
204207
});
205208

206209
return MaterialApp.router(
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:jplayer/src/data/storages/playback_storage.dart';
3+
4+
final playbackStorageProvider = Provider<PlaybackStorage>(
5+
(_) => PlaybackStorage(),
6+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export 'dio_provider.dart';
22
export 'download_database_provider.dart';
33
export 'jellyfin_api_provider.dart';
4+
export 'playback_storage_provider.dart';
45
export 'search_provider.dart';
56
export 'secure_storage_provider.dart';
67
export 'shared_preferences_provider.dart';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'dart:convert';
2+
3+
import 'package:jplayer/src/data/dto/dto.dart';
4+
import 'package:shared_preferences/shared_preferences.dart';
5+
6+
class PlaybackSnapshot {
7+
const PlaybackSnapshot({
8+
required this.songs,
9+
required this.album,
10+
required this.songId,
11+
required this.positionMs,
12+
});
13+
14+
final List<ItemDTO> songs;
15+
final ItemDTO album;
16+
final String songId;
17+
final int positionMs;
18+
}
19+
20+
class PlaybackStorage {
21+
static const _songsKey = 'pb_songs';
22+
static const _albumKey = 'pb_album';
23+
static const _songIdKey = 'pb_song_id';
24+
static const _positionMsKey = 'pb_position_ms';
25+
26+
Future<void> save({
27+
required List<ItemDTO> songs,
28+
required ItemDTO album,
29+
required String songId,
30+
required int positionMs,
31+
}) async {
32+
try {
33+
final prefs = await SharedPreferences.getInstance();
34+
final songsEncoded = jsonEncode(songs.map((s) => s.toJson()).toList());
35+
final albumEncoded = jsonEncode(album.toJson());
36+
await Future.wait([
37+
prefs.setString(_songsKey, songsEncoded),
38+
prefs.setString(_albumKey, albumEncoded),
39+
prefs.setString(_songIdKey, songId),
40+
prefs.setInt(_positionMsKey, positionMs),
41+
]);
42+
} on Object {
43+
// ignore
44+
}
45+
}
46+
47+
Future<PlaybackSnapshot?> load() async {
48+
try {
49+
final prefs = await SharedPreferences.getInstance();
50+
final songsJson = prefs.getString(_songsKey);
51+
final albumJson = prefs.getString(_albumKey);
52+
final songId = prefs.getString(_songIdKey);
53+
final positionMs = prefs.getInt(_positionMsKey) ?? 0;
54+
55+
if (songsJson == null || albumJson == null || songId == null) return null;
56+
57+
final songs = (jsonDecode(songsJson) as List)
58+
.map((e) => ItemDTO.fromJson(e as Map<String, dynamic>))
59+
.toList();
60+
final album = ItemDTO.fromJson(jsonDecode(albumJson) as Map<String, dynamic>);
61+
return PlaybackSnapshot(
62+
songs: songs,
63+
album: album,
64+
songId: songId,
65+
positionMs: positionMs,
66+
);
67+
} on Object {
68+
return null;
69+
}
70+
}
71+
}

‎lib/src/domain/providers/playback_provider.dart‎

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:jplayer/main.dart';
77
import 'package:jplayer/src/data/dto/dto.dart';
88
import 'package:jplayer/src/data/params/params.dart';
99
import 'package:jplayer/src/data/providers/providers.dart';
10+
import 'package:jplayer/src/data/storages/playback_storage.dart';
1011
import 'package:jplayer/src/domain/models/models.dart';
1112
import 'package:jplayer/src/domain/providers/current_user_provider.dart';
1213
import 'package:jplayer/src/domain/providers/download_manager_provider.dart';
@@ -52,17 +53,30 @@ class PlaybackNotifier extends StateNotifier<PlaybackState> {
5253
mediaSourceId: state.album?.id,
5354
),
5455
);
56+
final album = state.album;
57+
if (album != null) {
58+
unawaited(_saveToStorage(songId: nextSong.id, positionMs: 0));
59+
}
5560
}
5661
}
5762
}
5863
})
59-
// Listen for song completion
6064
..positionStream.listen((position) {
6165
state = state.copyWith(
6266
position: position,
6367
totalDuration: _audioPlayer.duration,
6468
currentMediaIndex: _audioPlayer.currentIndex,
6569
);
70+
71+
final now = DateTime.now();
72+
if (state.status.isPlaying && (_lastPositionSave == null || now.difference(_lastPositionSave!).inSeconds >= 5)) {
73+
_lastPositionSave = now;
74+
final currentIndex = _audioPlayer.currentIndex;
75+
final currentSong = currentIndex != null ? state.songs.elementAtOrNull(currentIndex) : null;
76+
if (currentSong != null) {
77+
unawaited(_saveToStorage(songId: currentSong.id, positionMs: position.inMilliseconds));
78+
}
79+
}
6680
})
6781
// Handle other player states as needed
6882
..playerStateStream.listen((playerState) {
@@ -78,12 +92,15 @@ class PlaybackNotifier extends StateNotifier<PlaybackState> {
7892
final Ref _ref;
7993
late AudioPlayer _audioPlayer;
8094
var _playSessionId = '';
95+
DateTime? _lastPositionSave;
8196

8297
Future<void> play(
8398
ItemDTO playSong,
8499
List<ItemDTO> songs,
85-
ItemDTO album,
86-
) async {
100+
ItemDTO album, {
101+
Duration? initialPosition,
102+
bool autoPlay = true,
103+
}) async {
87104
try {
88105
final audioSources = await Future.wait(
89106
songs.map((song) async {
@@ -145,19 +162,26 @@ class PlaybackNotifier extends StateNotifier<PlaybackState> {
145162
);
146163

147164
_playSessionId = DateTime.now().toIso8601String(); // Any unique ID
165+
final startIndex = songs.indexWhere((s) => s.id == playSong.id);
166+
final startPosition = initialPosition ?? Duration.zero;
148167
await _audioPlayer.setAudioSources(
149168
audioSources,
150-
initialIndex: songs.indexOf(playSong),
151-
preload: false,
169+
initialIndex: startIndex >= 0 ? startIndex : 0,
170+
initialPosition: startPosition,
171+
preload: true,
152172
);
173+
153174
state = state.copyWith(
154175
songs: songs,
155176
album: album,
156-
status: PlaybackStatus.playing,
157-
position: Duration.zero,
177+
status: autoPlay ? PlaybackStatus.playing : PlaybackStatus.paused,
178+
position: startPosition,
158179
totalDuration: _audioPlayer.duration,
180+
currentMediaIndex: startIndex >= 0 ? startIndex : 0,
159181
);
160-
unawaited(_audioPlayer.play());
182+
183+
unawaited(_saveToStorage(songId: playSong.id, positionMs: startPosition.inMilliseconds, songs: songs, album: album));
184+
if (autoPlay) unawaited(_audioPlayer.play());
161185
} catch (e) {
162186
if (e.toString().indexOf('setPitch') > 0) {
163187
// This is hack to avoid playback state being error on ios*`
@@ -176,6 +200,11 @@ class PlaybackNotifier extends StateNotifier<PlaybackState> {
176200
Future<void> seek(Duration position) async {
177201
await _audioPlayer.seek(position);
178202
state = state.copyWith(position: position);
203+
final currentIndex = _audioPlayer.currentIndex;
204+
final currentSong = currentIndex != null ? state.songs.elementAtOrNull(currentIndex) : null;
205+
if (currentSong != null) {
206+
unawaited(_saveToStorage(songId: currentSong.id, positionMs: position.inMilliseconds));
207+
}
179208
}
180209

181210
Future<void> playPause() => switch (state.status.isPlaying) {
@@ -272,6 +301,46 @@ class PlaybackNotifier extends StateNotifier<PlaybackState> {
272301
}
273302
}
274303

304+
Future<void> _saveToStorage({
305+
required String songId,
306+
required int positionMs,
307+
List<ItemDTO>? songs,
308+
ItemDTO? album,
309+
}) async {
310+
final effectiveSongs = songs ?? state.songs;
311+
final effectiveAlbum = album ?? state.album;
312+
if (effectiveSongs.isEmpty || effectiveAlbum == null) {
313+
return;
314+
}
315+
await PlaybackStorage().save(
316+
songs: effectiveSongs,
317+
album: effectiveAlbum,
318+
songId: songId,
319+
positionMs: positionMs,
320+
);
321+
}
322+
323+
Future<bool> tryRestore() async {
324+
final snapshot = await PlaybackStorage().load();
325+
if (snapshot == null) {
326+
return false;
327+
}
328+
329+
final song = snapshot.songs.firstWhere(
330+
(s) => s.id == snapshot.songId,
331+
orElse: () => snapshot.songs.first,
332+
);
333+
334+
await play(
335+
song,
336+
snapshot.songs,
337+
snapshot.album,
338+
initialPosition: Duration(milliseconds: snapshot.positionMs),
339+
autoPlay: false,
340+
);
341+
return true;
342+
}
343+
275344
@override
276345
void dispose() {
277346
_audioPlayer.dispose();

0 commit comments

Comments
 (0)