播放列表持久化设计、播放列表点击显示当前播放歌曲的列表内位置
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// lib/services/audio_service.dart
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -33,6 +34,13 @@ class Song {
|
||||
this.url,
|
||||
this.artwork,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'artist': artist,
|
||||
'url': url,
|
||||
};
|
||||
}
|
||||
|
||||
class AudioService extends ChangeNotifier {
|
||||
@@ -70,6 +78,9 @@ class AudioService extends ChangeNotifier {
|
||||
|
||||
int _playbackGeneration = 0;
|
||||
|
||||
// ---- 待恢复的播放进度 ----
|
||||
Duration? _pendingSeekPosition;
|
||||
|
||||
void Function(Song)? _onSongChanged;
|
||||
|
||||
// ---- Repository ----
|
||||
@@ -121,6 +132,7 @@ class AudioService extends ChangeNotifier {
|
||||
if (_currentPlaylistId != null) {
|
||||
_playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode);
|
||||
}
|
||||
savePlaybackState(); // 模式改变时保存
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
@@ -159,6 +171,13 @@ class AudioService extends ChangeNotifier {
|
||||
_shuffledIndex = -1;
|
||||
_currentPlaylistId = null;
|
||||
stopPlay();
|
||||
// 清空队列时也清除持久化状态
|
||||
_db.savePlaybackState(
|
||||
queueJson: [],
|
||||
currentIndex: 0,
|
||||
playMode: 'sequential',
|
||||
positionMs: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> playSong(Song song) async {
|
||||
@@ -255,12 +274,25 @@ class AudioService extends ChangeNotifier {
|
||||
await _playWithHeaders(song.url!);
|
||||
await _waitForPlaybackStarted();
|
||||
|
||||
// ⭐ 如果有待恢复的进度,执行 seek
|
||||
if (_pendingSeekPosition != null &&
|
||||
_pendingSeekPosition!.inMilliseconds > 0) {
|
||||
final pos = _pendingSeekPosition!;
|
||||
_pendingSeekPosition = null;
|
||||
await PlaybackService().seek(pos);
|
||||
positionNotifier.value = pos;
|
||||
debugPrint('🎯 [AudioService] restored position: $pos');
|
||||
}
|
||||
|
||||
_syncPlayerStateDelayed();
|
||||
|
||||
if (_hasStartedCurrentPlayback) {
|
||||
_onSongChanged?.call(song);
|
||||
_loadMetadataForCurrentSong(generation);
|
||||
}
|
||||
|
||||
// 切歌完成后保存状态
|
||||
await savePlaybackState();
|
||||
} finally {
|
||||
_isChangingTrack = false;
|
||||
}
|
||||
@@ -355,7 +387,7 @@ class AudioService extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ 第四层:_currentSong 校验(防止状态不同步)
|
||||
// 第四层:_currentSong 校验
|
||||
if (_currentSong == null || _currentSong!.id != songId) {
|
||||
debugPrint('⚠️ [AudioService] metadata stale: current song mismatch');
|
||||
return;
|
||||
@@ -510,6 +542,8 @@ class AudioService extends ChangeNotifier {
|
||||
} else {
|
||||
PlaybackService().resume();
|
||||
}
|
||||
// 保存状态(包括进度)
|
||||
savePlaybackState();
|
||||
}
|
||||
|
||||
void stopPlay() {
|
||||
@@ -520,6 +554,8 @@ class AudioService extends ChangeNotifier {
|
||||
bufferedNotifier.value = Duration.zero;
|
||||
_stopListening();
|
||||
notifyListeners();
|
||||
// 停止时也保存一次
|
||||
savePlaybackState();
|
||||
}
|
||||
|
||||
void seekTo(Duration position) {
|
||||
@@ -530,6 +566,8 @@ class AudioService extends ChangeNotifier {
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
_isUserSeeking = false;
|
||||
});
|
||||
// 拖动后保存进度
|
||||
savePlaybackState();
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
@@ -584,6 +622,8 @@ class AudioService extends ChangeNotifier {
|
||||
if (_isPlaying != playing) {
|
||||
_isPlaying = playing;
|
||||
notifyListeners();
|
||||
// 播放状态变化时保存进度(暂停时已保存,但播放开始也可保存一次)
|
||||
if (playing) savePlaybackState();
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -682,7 +722,6 @@ class AudioService extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ 关键判断:是否是最后一首
|
||||
final isLastSong = _currentIndex + 1 >= _queue.length;
|
||||
if (isLastSong && _playMode != PlayMode.repeatOne) {
|
||||
debugPrint(
|
||||
@@ -707,8 +746,133 @@ class AudioService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// ⭐ 播放状态持久化
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
/// 保存当前播放状态
|
||||
Future<void> savePlaybackState() async {
|
||||
if (_queue.isEmpty) {
|
||||
// 空队列时保存空状态
|
||||
await _db.savePlaybackState(
|
||||
queueJson: [],
|
||||
currentIndex: 0,
|
||||
playMode: 'sequential',
|
||||
positionMs: 0,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final queueJson = _queue.map((song) => song.toJson()).toList();
|
||||
final modeStr = _playMode == PlayMode.sequential
|
||||
? 'sequential'
|
||||
: _playMode == PlayMode.repeatOne
|
||||
? 'repeat_one'
|
||||
: 'shuffle';
|
||||
await _db.savePlaybackState(
|
||||
queueJson: queueJson,
|
||||
currentIndex: _currentIndex,
|
||||
playMode: modeStr,
|
||||
currentPlaylistId: _currentPlaylistId,
|
||||
positionMs: positionNotifier.value.inMilliseconds,
|
||||
);
|
||||
debugPrint('💾 [AudioService] playback state saved');
|
||||
}
|
||||
|
||||
/// 恢复播放状态
|
||||
/// 恢复播放状态
|
||||
Future<bool> restorePlaybackState() async {
|
||||
final state = await _db.getPlaybackState();
|
||||
if (state == null) return false;
|
||||
|
||||
try {
|
||||
final queueJson = jsonDecode(state['queue_json'] as String) as List;
|
||||
if (queueJson.isEmpty) return false;
|
||||
|
||||
final queue = queueJson
|
||||
.map((item) => Song(
|
||||
id: item['id'] as String,
|
||||
title: item['title'] as String? ?? '',
|
||||
artist: item['artist'] as String? ?? '未知艺术家',
|
||||
url: item['url'] as String?,
|
||||
artwork: null,
|
||||
))
|
||||
.toList();
|
||||
|
||||
if (queue.isEmpty) return false;
|
||||
|
||||
_queue = queue;
|
||||
_currentIndex = state['current_index'] as int;
|
||||
if (_currentIndex >= _queue.length) _currentIndex = 0;
|
||||
|
||||
final modeStr = state['play_mode'] as String? ?? 'sequential';
|
||||
_playMode = modeStr == 'sequential'
|
||||
? PlayMode.sequential
|
||||
: modeStr == 'repeat_one'
|
||||
? PlayMode.repeatOne
|
||||
: PlayMode.shuffle;
|
||||
_currentPlaylistId = state['current_playlist_id'] as String?;
|
||||
|
||||
// 保存待恢复的进度
|
||||
final savedPos = state['position_ms'] as int? ?? 0;
|
||||
_pendingSeekPosition = Duration(milliseconds: savedPos);
|
||||
|
||||
final song = _queue[_currentIndex];
|
||||
_currentSong = song;
|
||||
_onSongChanged?.call(song);
|
||||
|
||||
debugPrint(
|
||||
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
|
||||
|
||||
// ⭐ 关键:加载音频但不自动播放
|
||||
await _loadRestoredPlayback();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [AudioService] restore playback state failed: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ⭐ 加载恢复的播放状态(加载音频,不自动播放)
|
||||
Future<void> _loadRestoredPlayback() async {
|
||||
if (_queue.isEmpty || _currentIndex < 0) return;
|
||||
final song = _queue[_currentIndex];
|
||||
if (song.url == null || song.url!.isEmpty) return;
|
||||
|
||||
// 启动监听
|
||||
_startListening();
|
||||
|
||||
// 加载音频(带 headers)
|
||||
final headers = await _getAuthHeadersForUrl(song.url!);
|
||||
await PlaybackService().play(song.url!, headers: headers);
|
||||
|
||||
// 等待播放器准备好
|
||||
await _waitForPlaybackStarted();
|
||||
|
||||
// 如果有待恢复的进度,执行 seek
|
||||
if (_pendingSeekPosition != null &&
|
||||
_pendingSeekPosition!.inMilliseconds > 0) {
|
||||
final pos = _pendingSeekPosition!;
|
||||
_pendingSeekPosition = null;
|
||||
await PlaybackService().seek(pos);
|
||||
positionNotifier.value = pos;
|
||||
debugPrint('🎯 [AudioService] restored position: $pos');
|
||||
}
|
||||
|
||||
// ⭐ 加载完成后立即暂停(用户点击播放按钮后才继续)
|
||||
await PlaybackService().pause();
|
||||
_isPlaying = false;
|
||||
_hasStartedCurrentPlayback = true;
|
||||
notifyListeners();
|
||||
|
||||
debugPrint('🎵 [AudioService] restored playback loaded and paused');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 先保存再清理
|
||||
savePlaybackState();
|
||||
_stopListening();
|
||||
positionNotifier.dispose();
|
||||
durationNotifier.dispose();
|
||||
|
||||
Reference in New Issue
Block a user