播放列表持久化设计、播放列表点击显示当前播放歌曲的列表内位置

This commit is contained in:
2026-08-28 22:16:55 +08:00
parent f91846be9a
commit fa396ce681
4 changed files with 424 additions and 54 deletions
+166 -2
View File
@@ -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();