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

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
+70 -24
View File
@@ -1,8 +1,10 @@
// lib/database/song_database.dart // lib/database/song_database.dart
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../services/audio_service.dart'; // 用于 PlayMode 枚举,但为了解耦我们传字符串
class SongDatabase { class SongDatabase {
static final SongDatabase _instance = SongDatabase._internal(); static final SongDatabase _instance = SongDatabase._internal();
@@ -11,7 +13,6 @@ class SongDatabase {
static Database? _database; static Database? _database;
// ⭐ 开启/关闭日志(开发阶段开启)
static bool enableLog = true; static bool enableLog = true;
Future<Database> get database async { Future<Database> get database async {
@@ -26,19 +27,14 @@ class SongDatabase {
_log('📂 数据库路径: $path'); _log('📂 数据库路径: $path');
return await openDatabase( return await openDatabase(
path, path,
version: 3, version: 4, // 升级版本号
onCreate: _onCreate, onCreate: _onCreate,
onUpgrade: _onUpgrade, onUpgrade: _onUpgrade,
); );
} }
// ════════════════════════════════════════════════════════════
// 日志工具
// ════════════════════════════════════════════════════════════
void _log(String message) { void _log(String message) {
if (enableLog) { if (enableLog) debugPrint('📦 [DB] $message');
debugPrint('📦 [DB] $message');
}
} }
void _logQuery(String table, String operation, {Map<String, dynamic>? args}) { void _logQuery(String table, String operation, {Map<String, dynamic>? args}) {
@@ -48,9 +44,6 @@ class SongDatabase {
} }
} }
// ════════════════════════════════════════════════════════════
// 辅助方法
// ════════════════════════════════════════════════════════════
Future<bool> _tableExists(Database db, String tableName) async { Future<bool> _tableExists(Database db, String tableName) async {
final result = await db.query( final result = await db.query(
'sqlite_master', 'sqlite_master',
@@ -66,9 +59,6 @@ class SongDatabase {
return result.any((col) => col['name'] == columnName); return result.any((col) => col['name'] == columnName);
} }
// ════════════════════════════════════════════════════════════
// 创建表
// ════════════════════════════════════════════════════════════
Future<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
_log('🆕 创建数据库 (version $version)'); _log('🆕 创建数据库 (version $version)');
await _createSongsTable(db); await _createSongsTable(db);
@@ -76,6 +66,7 @@ class SongDatabase {
await _createPlaylistsTable(db); await _createPlaylistsTable(db);
await _createPlaylistSongsTable(db); await _createPlaylistSongsTable(db);
await _createFavoritesTable(db); await _createFavoritesTable(db);
await _createPlaybackStateTable(db);
await _createIndexes(db); await _createIndexes(db);
_log('✅ 数据库创建完成'); _log('✅ 数据库创建完成');
} }
@@ -155,6 +146,21 @@ class SongDatabase {
_log('📋 表创建: favorites'); _log('📋 表创建: favorites');
} }
Future<void> _createPlaybackStateTable(Database db) async {
await db.execute('''
CREATE TABLE playback_state (
id INTEGER PRIMARY KEY,
queue_json TEXT,
current_index INTEGER,
play_mode TEXT DEFAULT 'sequential',
current_playlist_id TEXT,
position_ms INTEGER DEFAULT 0,
updated_at INTEGER
)
''');
_log('📋 表创建: playback_state');
}
Future<void> _createIndexes(Database db) async { Future<void> _createIndexes(Database db) async {
await db.execute( await db.execute(
'CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist)'); 'CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist)');
@@ -170,7 +176,7 @@ class SongDatabase {
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// 升级逻辑 // 升级逻辑(版本 3 → 4
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
_log('⬆️ 数据库升级: $oldVersion$newVersion'); _log('⬆️ 数据库升级: $oldVersion$newVersion');
@@ -180,7 +186,6 @@ class SongDatabase {
await db.execute('ALTER TABLE songs ADD COLUMN content_hash TEXT'); await db.execute('ALTER TABLE songs ADD COLUMN content_hash TEXT');
_log('🔧 添加列: songs.content_hash'); _log('🔧 添加列: songs.content_hash');
} }
final playlistsExists = await _tableExists(db, 'playlists'); final playlistsExists = await _tableExists(db, 'playlists');
if (!playlistsExists) { if (!playlistsExists) {
await _createPlaylistsTable(db); await _createPlaylistsTable(db);
@@ -195,13 +200,11 @@ class SongDatabase {
} }
await _createIndexes(db); await _createIndexes(db);
} }
if (oldVersion < 3) { if (oldVersion < 3) {
if (!await _columnExists(db, 'songs', 'content_hash')) { if (!await _columnExists(db, 'songs', 'content_hash')) {
await db.execute('ALTER TABLE songs ADD COLUMN content_hash TEXT'); await db.execute('ALTER TABLE songs ADD COLUMN content_hash TEXT');
_log('🔧 添加列: songs.content_hash'); _log('🔧 添加列: songs.content_hash');
} }
final playlistsExists = await _tableExists(db, 'playlists'); final playlistsExists = await _tableExists(db, 'playlists');
if (!playlistsExists) { if (!playlistsExists) {
await _createPlaylistsTable(db); await _createPlaylistsTable(db);
@@ -216,12 +219,18 @@ class SongDatabase {
} }
await _createIndexes(db); await _createIndexes(db);
} }
// ⭐ 升级到版本 4:添加 playback_state 表
if (oldVersion < 4) {
final exists = await _tableExists(db, 'playback_state');
if (!exists) {
await _createPlaybackStateTable(db);
}
}
_log('✅ 数据库升级完成'); _log('✅ 数据库升级完成');
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// 查询方法(带日志 // 查询方法(原有
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
Future<Map<String, dynamic>?> getSong(String songKey) async { Future<Map<String, dynamic>?> getSong(String songKey) async {
_logQuery('songs', 'get', args: {'song_key': songKey}); _logQuery('songs', 'get', args: {'song_key': songKey});
@@ -261,7 +270,7 @@ class SongDatabase {
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// 播放列表 CRUD带日志 // 播放列表 CRUD原有
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
Future<List<Map<String, dynamic>>> getAllPlaylists() async { Future<List<Map<String, dynamic>>> getAllPlaylists() async {
_logQuery('playlists', 'getAll'); _logQuery('playlists', 'getAll');
@@ -298,7 +307,7 @@ class SongDatabase {
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// 播放列表歌曲(带日志 // 播放列表歌曲(原有
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
Future<List<Map<String, dynamic>>> getPlaylistSongs(String playlistId) async { Future<List<Map<String, dynamic>>> getPlaylistSongs(String playlistId) async {
_logQuery('playlist_songs', 'get', args: {'playlist_id': playlistId}); _logQuery('playlist_songs', 'get', args: {'playlist_id': playlistId});
@@ -359,7 +368,7 @@ class SongDatabase {
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// 收藏(带日志 // 收藏(原有
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
Future<List<Map<String, dynamic>>> getFavorites() async { Future<List<Map<String, dynamic>>> getFavorites() async {
_logQuery('favorites', 'getAll'); _logQuery('favorites', 'getAll');
@@ -403,7 +412,7 @@ class SongDatabase {
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// 原有方法(带日志 // 原有方法(歌曲 CRUD
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
Future<void> insertSong(Map<String, dynamic> song) async { Future<void> insertSong(Map<String, dynamic> song) async {
_logQuery('songs', 'insert', args: {'title': song['title']}); _logQuery('songs', 'insert', args: {'title': song['title']});
@@ -449,6 +458,43 @@ class SongDatabase {
return result.isNotEmpty ? result.first : null; return result.isNotEmpty ? result.first : null;
} }
// ════════════════════════════════════════════════════════════
// ⭐ 新增:播放状态持久化
// ════════════════════════════════════════════════════════════
/// 保存播放状态
Future<void> savePlaybackState({
required List<Map<String, dynamic>> queueJson, // 预先序列化的列表
required int currentIndex,
required String playMode,
String? currentPlaylistId,
required int positionMs,
}) async {
final db = await database;
await db.insert(
'playback_state',
{
'id': 1,
'queue_json': jsonEncode(queueJson),
'current_index': currentIndex,
'play_mode': playMode,
'current_playlist_id': currentPlaylistId,
'position_ms': positionMs,
'updated_at': DateTime.now().millisecondsSinceEpoch,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
_log('💾 播放状态已保存');
}
/// 获取播放状态
Future<Map<String, dynamic>?> getPlaybackState() async {
final db = await database;
final result =
await db.query('playback_state', where: 'id = ?', whereArgs: [1]);
return result.isNotEmpty ? result.first : null;
}
Future<void> close() async { Future<void> close() async {
_log('🔒 关闭数据库连接'); _log('🔒 关闭数据库连接');
final db = await database; final db = await database;
+4
View File
@@ -80,6 +80,10 @@ void main() async {
); );
debugPrint('⏱️ T0.8 runApp 完成: ${stopwatch.elapsedMilliseconds}ms'); debugPrint('⏱️ T0.8 runApp 完成: ${stopwatch.elapsedMilliseconds}ms');
WidgetsBinding.instance.addPostFrameCallback((_) {
AudioService().restorePlaybackState();
});
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
+183 -27
View File
@@ -1,11 +1,175 @@
// lib/pages/playlist_page.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/audio_service.dart'; import '../services/audio_service.dart';
import '../constants/ui_constants.dart';
class PlaylistPage extends StatelessWidget { class PlaylistPage extends StatefulWidget {
const PlaylistPage({super.key}); const PlaylistPage({super.key});
@override
State<PlaylistPage> createState() => _PlaylistPageState();
}
class _PlaylistPageState extends State<PlaylistPage> {
final ScrollController _scrollController = ScrollController();
final GlobalKey _currentTagKey = GlobalKey();
bool _hasScrolledToCurrent = false;
int _retryCount = 0;
static const int _maxRetries = 3;
// ⭐ 统一滚动时长 1.5 秒
static const Duration _scrollDuration = Duration(milliseconds: 1500);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToCurrentSong();
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
double _calculateAverageItemHeight(int itemCount) {
final maxExtent = _scrollController.position.maxScrollExtent;
if (maxExtent <= 0 || itemCount <= 0) return 72.0;
return maxExtent / itemCount;
}
double _calculateTargetOffset(
int currentIndex, int totalCount, double avgHeight) {
final viewportHeight = _scrollController.position.viewportDimension;
return (currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2);
}
Future<void> _scrollToCurrentSong() async {
if (_hasScrolledToCurrent) return;
final service = context.read<AudioService>();
final queue = service.queue;
final currentIndex = service.currentIndex;
if (queue.isEmpty || currentIndex < 0 || currentIndex >= queue.length) {
return;
}
await Future.delayed(const Duration(milliseconds: 300));
if (!mounted) return;
final totalCount = queue.length;
final avgHeight = _calculateAverageItemHeight(totalCount);
final maxExtent = _scrollController.position.maxScrollExtent;
var tagContext = _currentTagKey.currentContext;
if (tagContext != null) {
await _calibrateWithTag(tagContext);
_hasScrolledToCurrent = true;
return;
}
// ⭐ 粗定位:1.5 秒平滑滚动
final targetOffset =
_calculateTargetOffset(currentIndex, totalCount, avgHeight);
final clampedOffset = targetOffset.clamp(0.0, maxExtent);
await _scrollController.animateTo(
clampedOffset,
duration: _scrollDuration,
curve: Curves.easeOutCubic,
);
if (!mounted) return;
await WidgetsBinding.instance.endOfFrame;
if (!mounted) return;
tagContext = _currentTagKey.currentContext;
if (tagContext != null) {
await _calibrateWithTag(tagContext);
_hasScrolledToCurrent = true;
return;
}
// ⭐ 迭代修正:每次 1.5 秒平滑滚动
_retryCount = 0;
var currentOffset = _scrollController.offset;
var lastOffset = currentOffset;
while (_retryCount < _maxRetries) {
_retryCount++;
final viewportHeight = _scrollController.position.viewportDimension;
final step = viewportHeight * 0.5;
final direction = (currentIndex > totalCount / 2) ? -1 : 1;
final newOffset =
(currentOffset + direction * step).clamp(0.0, maxExtent);
if ((newOffset - currentOffset).abs() < 50) {
final bigStep = viewportHeight * 0.8 * direction;
final forcedOffset = (currentOffset + bigStep).clamp(0.0, maxExtent);
await _scrollController.animateTo(
forcedOffset,
duration: _scrollDuration,
curve: Curves.easeOutCubic,
);
} else {
await _scrollController.animateTo(
newOffset,
duration: _scrollDuration,
curve: Curves.easeOutCubic,
);
}
if (!mounted) return;
await WidgetsBinding.instance.endOfFrame;
if (!mounted) return;
tagContext = _currentTagKey.currentContext;
if (tagContext != null) {
await _calibrateWithTag(tagContext);
_hasScrolledToCurrent = true;
return;
}
currentOffset = _scrollController.offset;
if ((currentOffset - lastOffset).abs() < 10) break;
lastOffset = currentOffset;
}
_hasScrolledToCurrent = true;
}
/// 精确定位:1.5 秒平滑滚动
Future<void> _calibrateWithTag(BuildContext tagContext) async {
final renderBox = tagContext.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final tagPosition = renderBox.localToGlobal(Offset.zero);
final tagSize = renderBox.size;
final tagCenter = tagPosition.dy + tagSize.height / 2;
final screenHeight = MediaQuery.of(context).size.height;
final screenCenter = screenHeight / 2;
final delta = tagCenter - screenCenter;
final currentOffset = _scrollController.offset;
final targetOffset = (currentOffset + delta)
.clamp(0.0, _scrollController.position.maxScrollExtent);
if ((targetOffset - currentOffset).abs() < 2) return;
await _scrollController.animateTo(
targetOffset,
duration: _scrollDuration,
curve: Curves.easeOutCubic,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final service = context.watch<AudioService>(); final service = context.watch<AudioService>();
@@ -31,6 +195,10 @@ class PlaylistPage extends StatelessWidget {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
actions: [ actions: [
IconButton(
icon: Icon(service.playModeIcon, color: Colors.white54),
onPressed: service.togglePlayMode,
),
IconButton( IconButton(
icon: const Icon(Icons.clear_all, color: Colors.white54), icon: const Icon(Icons.clear_all, color: Colors.white54),
onPressed: () { onPressed: () {
@@ -38,29 +206,22 @@ class PlaylistPage extends StatelessWidget {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: const Color(0xFF1A1F1E), backgroundColor: const Color(0xFF1A1F1E),
title: const Text( title: const Text('清空播放列表',
'清空播放列表', style: TextStyle(color: Colors.white)),
style: TextStyle(color: Colors.white), content: const Text('确定要清空当前播放列表吗?',
), style: TextStyle(color: Colors.grey)),
content: const Text(
'确定要清空当前播放列表吗?',
style: TextStyle(color: Colors.grey),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('取消'), child: const Text('取消')),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
service.clearQueue(); service.clearQueue();
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
}, },
child: const Text( child:
'清空', const Text('清空', style: TextStyle(color: Colors.red)),
style: TextStyle(color: Colors.red),
),
), ),
], ],
), ),
@@ -76,20 +237,13 @@ class PlaylistPage extends StatelessWidget {
children: [ children: [
Icon(Icons.playlist_play, size: 48, color: Colors.grey), Icon(Icons.playlist_play, size: 48, color: Colors.grey),
SizedBox(height: 16), SizedBox(height: 16),
Text( Text('播放列表为空', style: TextStyle(color: Colors.grey)),
'播放列表为空',
style: TextStyle(color: Colors.grey),
),
], ],
), ),
) )
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.only( controller: _scrollController,
left: 16, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
right: 16,
top: 8,
bottom: UIConstants.miniPlayerBottomSpace,
),
itemCount: queue.length, itemCount: queue.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final song = queue[index]; final song = queue[index];
@@ -127,12 +281,14 @@ class PlaylistPage extends StatelessWidget {
), ),
trailing: isCurrent trailing: isCurrent
? Container( ? Container(
key: _currentTagKey,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFB8D4D0).withOpacity(0.2), color:
const Color(0xFFB8D4D0).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: const Text( child: const Text(
+166 -2
View File
@@ -1,5 +1,6 @@
// lib/services/audio_service.dart // lib/services/audio_service.dart
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -33,6 +34,13 @@ class Song {
this.url, this.url,
this.artwork, this.artwork,
}); });
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'artist': artist,
'url': url,
};
} }
class AudioService extends ChangeNotifier { class AudioService extends ChangeNotifier {
@@ -70,6 +78,9 @@ class AudioService extends ChangeNotifier {
int _playbackGeneration = 0; int _playbackGeneration = 0;
// ---- 待恢复的播放进度 ----
Duration? _pendingSeekPosition;
void Function(Song)? _onSongChanged; void Function(Song)? _onSongChanged;
// ---- Repository ---- // ---- Repository ----
@@ -121,6 +132,7 @@ class AudioService extends ChangeNotifier {
if (_currentPlaylistId != null) { if (_currentPlaylistId != null) {
_playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode); _playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode);
} }
savePlaybackState(); // 模式改变时保存
} }
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
@@ -159,6 +171,13 @@ class AudioService extends ChangeNotifier {
_shuffledIndex = -1; _shuffledIndex = -1;
_currentPlaylistId = null; _currentPlaylistId = null;
stopPlay(); stopPlay();
// 清空队列时也清除持久化状态
_db.savePlaybackState(
queueJson: [],
currentIndex: 0,
playMode: 'sequential',
positionMs: 0,
);
} }
Future<void> playSong(Song song) async { Future<void> playSong(Song song) async {
@@ -255,12 +274,25 @@ class AudioService extends ChangeNotifier {
await _playWithHeaders(song.url!); await _playWithHeaders(song.url!);
await _waitForPlaybackStarted(); 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(); _syncPlayerStateDelayed();
if (_hasStartedCurrentPlayback) { if (_hasStartedCurrentPlayback) {
_onSongChanged?.call(song); _onSongChanged?.call(song);
_loadMetadataForCurrentSong(generation); _loadMetadataForCurrentSong(generation);
} }
// 切歌完成后保存状态
await savePlaybackState();
} finally { } finally {
_isChangingTrack = false; _isChangingTrack = false;
} }
@@ -355,7 +387,7 @@ class AudioService extends ChangeNotifier {
return; return;
} }
// 第四层:_currentSong 校验(防止状态不同步) // 第四层:_currentSong 校验
if (_currentSong == null || _currentSong!.id != songId) { if (_currentSong == null || _currentSong!.id != songId) {
debugPrint('⚠️ [AudioService] metadata stale: current song mismatch'); debugPrint('⚠️ [AudioService] metadata stale: current song mismatch');
return; return;
@@ -510,6 +542,8 @@ class AudioService extends ChangeNotifier {
} else { } else {
PlaybackService().resume(); PlaybackService().resume();
} }
// 保存状态(包括进度)
savePlaybackState();
} }
void stopPlay() { void stopPlay() {
@@ -520,6 +554,8 @@ class AudioService extends ChangeNotifier {
bufferedNotifier.value = Duration.zero; bufferedNotifier.value = Duration.zero;
_stopListening(); _stopListening();
notifyListeners(); notifyListeners();
// 停止时也保存一次
savePlaybackState();
} }
void seekTo(Duration position) { void seekTo(Duration position) {
@@ -530,6 +566,8 @@ class AudioService extends ChangeNotifier {
Future.delayed(const Duration(milliseconds: 800), () { Future.delayed(const Duration(milliseconds: 800), () {
_isUserSeeking = false; _isUserSeeking = false;
}); });
// 拖动后保存进度
savePlaybackState();
} }
void clearQueue() { void clearQueue() {
@@ -584,6 +622,8 @@ class AudioService extends ChangeNotifier {
if (_isPlaying != playing) { if (_isPlaying != playing) {
_isPlaying = playing; _isPlaying = playing;
notifyListeners(); notifyListeners();
// 播放状态变化时保存进度(暂停时已保存,但播放开始也可保存一次)
if (playing) savePlaybackState();
} }
}), }),
); );
@@ -682,7 +722,6 @@ class AudioService extends ChangeNotifier {
return; return;
} }
// ⭐ 关键判断:是否是最后一首
final isLastSong = _currentIndex + 1 >= _queue.length; final isLastSong = _currentIndex + 1 >= _queue.length;
if (isLastSong && _playMode != PlayMode.repeatOne) { if (isLastSong && _playMode != PlayMode.repeatOne) {
debugPrint( 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 @override
void dispose() { void dispose() {
// 先保存再清理
savePlaybackState();
_stopListening(); _stopListening();
positionNotifier.dispose(); positionNotifier.dispose();
durationNotifier.dispose(); durationNotifier.dispose();