引入播放列表库,同时引入全新的播放控制器,以控制读库与播放模式的动作,但是功能仍在完善,需要确认部分边界问题
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
// lib/controllers/playback_controller.dart
|
||||||
|
import '../services/audio_service.dart';
|
||||||
|
import '../services/webdav_service.dart';
|
||||||
|
import '../database/song_database.dart';
|
||||||
|
|
||||||
|
class PlaybackController {
|
||||||
|
static final PlaybackController _instance = PlaybackController._internal();
|
||||||
|
factory PlaybackController() => _instance;
|
||||||
|
PlaybackController._internal();
|
||||||
|
|
||||||
|
final AudioService _audioService = AudioService();
|
||||||
|
final SongDatabase _db = SongDatabase();
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放 WebDAV 目录中的歌曲(从点击歌曲开始)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> playFromWebDAV({
|
||||||
|
required String directoryPath,
|
||||||
|
required String clickedSongPath,
|
||||||
|
required List<WebDAVItem> allItems,
|
||||||
|
}) async {
|
||||||
|
final musicFiles = allItems.where((item) => !item.isDirectory).toList();
|
||||||
|
if (musicFiles.isEmpty) return;
|
||||||
|
|
||||||
|
final clickedIndex =
|
||||||
|
musicFiles.indexWhere((item) => item.path == clickedSongPath);
|
||||||
|
if (clickedIndex == -1) {
|
||||||
|
await _playSingleSong(clickedSongPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final queue = await _buildSongQueue(musicFiles);
|
||||||
|
|
||||||
|
final playMode = _audioService.playMode;
|
||||||
|
// ⭐ 修复:显式指定类型
|
||||||
|
List<Song> finalQueue = List<Song>.from(queue);
|
||||||
|
int startIndex = clickedIndex;
|
||||||
|
|
||||||
|
if (playMode == PlayMode.shuffle) {
|
||||||
|
final shuffled = List<Song>.from(queue)..shuffle();
|
||||||
|
startIndex = shuffled.indexWhere((s) => s.id == clickedSongPath);
|
||||||
|
if (startIndex == -1) startIndex = 0;
|
||||||
|
finalQueue = shuffled;
|
||||||
|
}
|
||||||
|
|
||||||
|
_audioService.setQueue(finalQueue, startIndex: startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放整个目录(从第一首开始)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> playAllFromWebDAV({
|
||||||
|
required String directoryPath,
|
||||||
|
required List<WebDAVItem> allItems,
|
||||||
|
}) async {
|
||||||
|
final musicFiles = allItems.where((item) => !item.isDirectory).toList();
|
||||||
|
if (musicFiles.isEmpty) return;
|
||||||
|
|
||||||
|
final queue = await _buildSongQueue(musicFiles);
|
||||||
|
|
||||||
|
final playMode = _audioService.playMode;
|
||||||
|
// ⭐ 修复:显式指定类型
|
||||||
|
List<Song> finalQueue = List<Song>.from(queue);
|
||||||
|
if (playMode == PlayMode.shuffle) {
|
||||||
|
finalQueue = List<Song>.from(queue)..shuffle();
|
||||||
|
}
|
||||||
|
|
||||||
|
_audioService.setQueue(finalQueue, startIndex: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放单曲(fallback)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> _playSingleSong(String songPath) async {
|
||||||
|
final url = WebDAVService.instance.getFileUrl(songPath);
|
||||||
|
final dbSong = await _db.getSongByPath(songPath);
|
||||||
|
final song = Song(
|
||||||
|
id: songPath,
|
||||||
|
title: dbSong?['title'] as String? ??
|
||||||
|
songPath.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
||||||
|
artist: dbSong?['artist'] as String? ?? '未知艺术家',
|
||||||
|
url: url,
|
||||||
|
);
|
||||||
|
_audioService.playSong(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 辅助方法:从音乐文件列表构建 Song 队列
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<List<Song>> _buildSongQueue(List<WebDAVItem> musicFiles) async {
|
||||||
|
final queue = <Song>[];
|
||||||
|
for (final item in musicFiles) {
|
||||||
|
final dbSong = await _db.getSongByPath(item.path);
|
||||||
|
final url = WebDAVService.instance.getFileUrl(item.path);
|
||||||
|
if (dbSong != null) {
|
||||||
|
queue.add(Song(
|
||||||
|
id: item.path,
|
||||||
|
title: dbSong['title'] as String? ??
|
||||||
|
item.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
||||||
|
artist: dbSong['artist'] as String? ?? '未知艺术家',
|
||||||
|
url: url,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
queue.add(Song(
|
||||||
|
id: item.path,
|
||||||
|
title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
||||||
|
artist: '未知艺术家',
|
||||||
|
url: url,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放控制(透传到 AudioService)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
void next() {
|
||||||
|
_audioService.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
void previous() {
|
||||||
|
_audioService.previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
void togglePlayMode() {
|
||||||
|
_audioService.togglePlayMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayMode get playMode => _audioService.playMode;
|
||||||
|
}
|
||||||
+240
-14
@@ -21,16 +21,57 @@ class SongDatabase {
|
|||||||
final path = join(dir.path, 'qingting_songs.db');
|
final path = join(dir.path, 'qingting_songs.db');
|
||||||
return await openDatabase(
|
return await openDatabase(
|
||||||
path,
|
path,
|
||||||
version: 1,
|
version: 2,
|
||||||
onCreate: _onCreate,
|
onCreate: _onCreate,
|
||||||
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐ 新增:检查表是否存在
|
||||||
|
Future<bool> _tableExists(Database db, String tableName) async {
|
||||||
|
final result = await db.query(
|
||||||
|
'sqlite_master',
|
||||||
|
where: 'type = ? AND name = ?',
|
||||||
|
whereArgs: ['table', tableName],
|
||||||
|
);
|
||||||
|
return result.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onCreate(Database db, int version) async {
|
Future<void> _onCreate(Database db, int version) async {
|
||||||
|
await _createSongsTable(db);
|
||||||
|
await _createMetadataCacheTable(db);
|
||||||
|
await _createPlaylistsTable(db);
|
||||||
|
await _createPlaylistSongsTable(db);
|
||||||
|
await _createFavoritesTable(db);
|
||||||
|
await _createIndexes(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ 升级逻辑(从版本 1 升级到 2)
|
||||||
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
// 检查 playlists 表是否存在
|
||||||
|
final playlistsExists = await _tableExists(db, 'playlists');
|
||||||
|
|
||||||
|
if (!playlistsExists) {
|
||||||
|
// 表不存在 → 创建所有新表
|
||||||
|
await _createPlaylistsTable(db);
|
||||||
|
await _createPlaylistSongsTable(db);
|
||||||
|
await _createFavoritesTable(db);
|
||||||
|
await _createIndexes(db);
|
||||||
|
} else {
|
||||||
|
// 表已存在 → 只添加新字段
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE playlists ADD COLUMN play_mode TEXT DEFAULT "sequential"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createSongsTable(Database db) async {
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE songs (
|
CREATE TABLE songs (
|
||||||
song_key TEXT PRIMARY KEY,
|
song_key TEXT PRIMARY KEY,
|
||||||
remote_path TEXT,
|
remote_path TEXT,
|
||||||
|
content_hash TEXT,
|
||||||
file_size INTEGER,
|
file_size INTEGER,
|
||||||
modified_time INTEGER,
|
modified_time INTEGER,
|
||||||
etag TEXT,
|
etag TEXT,
|
||||||
@@ -46,7 +87,9 @@ class SongDatabase {
|
|||||||
metadata_status TEXT DEFAULT 'pending'
|
metadata_status TEXT DEFAULT 'pending'
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createMetadataCacheTable(Database db) async {
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE metadata_cache (
|
CREATE TABLE metadata_cache (
|
||||||
song_key TEXT PRIMARY KEY,
|
song_key TEXT PRIMARY KEY,
|
||||||
@@ -56,12 +99,59 @@ class SongDatabase {
|
|||||||
FOREIGN KEY (song_key) REFERENCES songs(song_key) ON DELETE CASCADE
|
FOREIGN KEY (song_key) REFERENCES songs(song_key) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
|
|
||||||
await db.execute('CREATE INDEX idx_songs_artist ON songs(artist)');
|
|
||||||
await db.execute('CREATE INDEX idx_songs_title ON songs(title)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 查询 ----
|
Future<void> _createPlaylistsTable(Database db) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE playlists (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
play_mode TEXT DEFAULT 'sequential',
|
||||||
|
created_at INTEGER,
|
||||||
|
updated_at INTEGER
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createPlaylistSongsTable(Database db) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE playlist_songs (
|
||||||
|
playlist_id TEXT,
|
||||||
|
song_id TEXT,
|
||||||
|
order_index INTEGER,
|
||||||
|
added_at INTEGER,
|
||||||
|
PRIMARY KEY (playlist_id, song_id),
|
||||||
|
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createFavoritesTable(Database db) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE favorites (
|
||||||
|
song_id TEXT PRIMARY KEY,
|
||||||
|
favorited_at INTEGER,
|
||||||
|
FOREIGN KEY (song_id) REFERENCES songs(remote_path) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createIndexes(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist)');
|
||||||
|
await db
|
||||||
|
.execute('CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title)');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_songs_content_hash ON songs(content_hash)');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_playlist_songs_playlist ON playlist_songs(playlist_id)');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_playlist_songs_order ON playlist_songs(order_index)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 查询方法(保持不变)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<Map<String, dynamic>?> getSong(String songKey) async {
|
Future<Map<String, dynamic>?> getSong(String songKey) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final result =
|
final result =
|
||||||
@@ -69,6 +159,20 @@ class SongDatabase {
|
|||||||
return result.isNotEmpty ? result.first : null;
|
return result.isNotEmpty ? result.first : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getSongByPath(String remotePath) async {
|
||||||
|
final db = await database;
|
||||||
|
final result = await db
|
||||||
|
.query('songs', where: 'remote_path = ?', whereArgs: [remotePath]);
|
||||||
|
return result.isNotEmpty ? result.first : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getSongByHash(String contentHash) async {
|
||||||
|
final db = await database;
|
||||||
|
final result = await db
|
||||||
|
.query('songs', where: 'content_hash = ?', whereArgs: [contentHash]);
|
||||||
|
return result.isNotEmpty ? result.first : null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getSongsByArtist(String artist,
|
Future<List<Map<String, dynamic>>> getSongsByArtist(String artist,
|
||||||
{int limit = 20}) async {
|
{int limit = 20}) async {
|
||||||
if (artist.isEmpty) return [];
|
if (artist.isEmpty) return [];
|
||||||
@@ -81,7 +185,135 @@ class SongDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 写入 ----
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放列表 CRUD(保持不变)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<List<Map<String, dynamic>>> getAllPlaylists() async {
|
||||||
|
final db = await database;
|
||||||
|
return await db.query('playlists', orderBy: 'created_at DESC');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getPlaylist(String id) async {
|
||||||
|
final db = await database;
|
||||||
|
final result =
|
||||||
|
await db.query('playlists', where: 'id = ?', whereArgs: [id]);
|
||||||
|
return result.isNotEmpty ? result.first : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insertPlaylist(Map<String, dynamic> playlist) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.insert('playlists', playlist,
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePlaylist(String id, Map<String, dynamic> updates) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.update('playlists', updates, where: 'id = ?', whereArgs: [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePlaylist(String id) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.delete('playlists', where: 'id = ?', whereArgs: [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放列表歌曲操作(保持不变)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<List<Map<String, dynamic>>> getPlaylistSongs(String playlistId) async {
|
||||||
|
final db = await database;
|
||||||
|
return await db.query(
|
||||||
|
'playlist_songs',
|
||||||
|
where: 'playlist_id = ?',
|
||||||
|
whereArgs: [playlistId],
|
||||||
|
orderBy: 'order_index ASC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addSongToPlaylist(String playlistId, String songId,
|
||||||
|
{int? orderIndex}) async {
|
||||||
|
final db = await database;
|
||||||
|
int finalOrder = orderIndex ?? 0;
|
||||||
|
if (orderIndex == null) {
|
||||||
|
final existing = await db.query(
|
||||||
|
'playlist_songs',
|
||||||
|
where: 'playlist_id = ?',
|
||||||
|
whereArgs: [playlistId],
|
||||||
|
orderBy: 'order_index DESC',
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
finalOrder =
|
||||||
|
existing.isNotEmpty ? (existing.first['order_index'] as int) + 1 : 0;
|
||||||
|
}
|
||||||
|
await db.insert(
|
||||||
|
'playlist_songs',
|
||||||
|
{
|
||||||
|
'playlist_id': playlistId,
|
||||||
|
'song_id': songId,
|
||||||
|
'order_index': finalOrder,
|
||||||
|
'added_at': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeSongFromPlaylist(String playlistId, String songId) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.delete(
|
||||||
|
'playlist_songs',
|
||||||
|
where: 'playlist_id = ? AND song_id = ?',
|
||||||
|
whereArgs: [playlistId, songId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearPlaylist(String playlistId) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.delete('playlist_songs',
|
||||||
|
where: 'playlist_id = ?', whereArgs: [playlistId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 收藏操作(保持不变)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<List<Map<String, dynamic>>> getFavorites() async {
|
||||||
|
final db = await database;
|
||||||
|
return await db.query('favorites', orderBy: 'favorited_at DESC');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isFavorite(String songId) async {
|
||||||
|
final db = await database;
|
||||||
|
final result =
|
||||||
|
await db.query('favorites', where: 'song_id = ?', whereArgs: [songId]);
|
||||||
|
return result.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addFavorite(String songId) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.insert(
|
||||||
|
'favorites',
|
||||||
|
{
|
||||||
|
'song_id': songId,
|
||||||
|
'favorited_at': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeFavorite(String songId) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.delete('favorites', where: 'song_id = ?', whereArgs: [songId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleFavorite(String songId) async {
|
||||||
|
if (await isFavorite(songId)) {
|
||||||
|
await removeFavorite(songId);
|
||||||
|
} else {
|
||||||
|
await addFavorite(songId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 原有写入/删除方法(保持不变)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> insertSong(Map<String, dynamic> song) async {
|
Future<void> insertSong(Map<String, dynamic> song) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
await db.insert('songs', song,
|
await db.insert('songs', song,
|
||||||
@@ -90,15 +322,10 @@ class SongDatabase {
|
|||||||
|
|
||||||
Future<void> updateSong(String songKey, Map<String, dynamic> updates) async {
|
Future<void> updateSong(String songKey, Map<String, dynamic> updates) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
await db.update(
|
await db
|
||||||
'songs',
|
.update('songs', updates, where: 'song_key = ?', whereArgs: [songKey]);
|
||||||
updates,
|
|
||||||
where: 'song_key = ?',
|
|
||||||
whereArgs: [songKey],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 删除 ----
|
|
||||||
Future<void> deleteSong(String songKey) async {
|
Future<void> deleteSong(String songKey) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
await db.delete('songs', where: 'song_key = ?', whereArgs: [songKey]);
|
await db.delete('songs', where: 'song_key = ?', whereArgs: [songKey]);
|
||||||
@@ -110,7 +337,6 @@ class SongDatabase {
|
|||||||
.delete('metadata_cache', where: 'song_key = ?', whereArgs: [songKey]);
|
.delete('metadata_cache', where: 'song_key = ?', whereArgs: [songKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 缓存 ----
|
|
||||||
Future<void> insertCache(Map<String, dynamic> cache) async {
|
Future<void> insertCache(Map<String, dynamic> cache) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
await db.insert('metadata_cache', cache,
|
await db.insert('metadata_cache', cache,
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// lib/models/playlist.dart
|
||||||
|
import 'package:qingting_player/services/audio_service.dart';
|
||||||
|
|
||||||
|
class Playlist {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final PlayMode playMode;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
Playlist({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.playMode = PlayMode.sequential,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) : createdAt = createdAt ?? DateTime.now(),
|
||||||
|
updatedAt = updatedAt ?? DateTime.now();
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'play_mode': Playlist.playModeToString(playMode), // ⭐ 改用公开静态方法
|
||||||
|
'created_at': createdAt.millisecondsSinceEpoch,
|
||||||
|
'updated_at': updatedAt.millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory Playlist.fromJson(Map<String, dynamic> json) => Playlist(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
playMode: Playlist.stringToPlayMode(
|
||||||
|
json['play_mode'] as String? ?? 'sequential'),
|
||||||
|
createdAt:
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
|
||||||
|
updatedAt:
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(json['updated_at'] as int),
|
||||||
|
);
|
||||||
|
|
||||||
|
Playlist copyWith({String? name, PlayMode? playMode}) => Playlist(
|
||||||
|
id: id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
playMode: playMode ?? this.playMode,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ⭐ 改为公开静态方法
|
||||||
|
static String playModeToString(PlayMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case PlayMode.sequential:
|
||||||
|
return 'sequential';
|
||||||
|
case PlayMode.repeatOne:
|
||||||
|
return 'repeat_one';
|
||||||
|
case PlayMode.shuffle:
|
||||||
|
return 'shuffle';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlayMode stringToPlayMode(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'repeat_one':
|
||||||
|
return PlayMode.repeatOne;
|
||||||
|
case 'shuffle':
|
||||||
|
return PlayMode.shuffle;
|
||||||
|
default:
|
||||||
|
return PlayMode.sequential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
// lib/pages/webdav_file_list_page.dart
|
// lib/pages/webdav_file_list_page.dart
|
||||||
// ignore: unused_import
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/webdav_service.dart';
|
import '../services/webdav_service.dart';
|
||||||
@@ -8,6 +6,7 @@ import '../services/playback_service.dart';
|
|||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
import '../constants/ui_constants.dart';
|
import '../constants/ui_constants.dart';
|
||||||
import '../base/base_state.dart';
|
import '../base/base_state.dart';
|
||||||
|
import '../controllers/playback_controller.dart';
|
||||||
|
|
||||||
class WebDAVFileListPage extends StatefulWidget {
|
class WebDAVFileListPage extends StatefulWidget {
|
||||||
final String currentPath;
|
final String currentPath;
|
||||||
@@ -53,16 +52,14 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
final items =
|
final items =
|
||||||
await WebDAVService.instance.listDirectory(path: _currentPath);
|
await WebDAVService.instance.listDirectory(path: _currentPath);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// ⭐ 加这个
|
safeSetState(() {
|
||||||
setState(() {
|
|
||||||
_items = items;
|
_items = items;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// ⭐ 加这个
|
safeSetState(() {
|
||||||
setState(() {
|
|
||||||
_errorMessage = '加载失败: $e';
|
_errorMessage = '加载失败: $e';
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
@@ -83,71 +80,21 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ⭐ 核心:播放歌曲 + 自动构建队列
|
// 播放单首(点击歌曲)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void _playSong(WebDAVItem file) async {
|
void _playSong(WebDAVItem file) async {
|
||||||
try {
|
try {
|
||||||
// 1. 获取当前目录所有音乐文件(过滤掉目录)
|
// ⭐ 直接使用 PlaybackController 单例
|
||||||
final musicFiles = _items.where((item) => !item.isDirectory).toList();
|
await PlaybackController().playFromWebDAV(
|
||||||
|
directoryPath: _currentPath,
|
||||||
if (musicFiles.isEmpty) {
|
clickedSongPath: file.path,
|
||||||
if (mounted) {
|
allItems: _items,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('当前目录没有音乐文件'),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 获取认证头
|
|
||||||
final headers = await WebDAVService.instance.getAuthHeaders();
|
|
||||||
|
|
||||||
// 3. 构建播放队列(所有音乐文件)
|
|
||||||
final queue = musicFiles.map((item) {
|
|
||||||
final itemUrl = WebDAVService.instance.getFileUrl(item.path);
|
|
||||||
return Song(
|
|
||||||
id: item.path,
|
|
||||||
title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
|
||||||
artist: '未知艺术家',
|
|
||||||
url: itemUrl,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
// 4. 找到当前点击歌曲在队列中的位置
|
|
||||||
final startIndex = queue.indexWhere((s) => s.id == file.path);
|
|
||||||
if (startIndex == -1) {
|
|
||||||
// 极端情况:队列构建有问题,退化为单曲播放
|
|
||||||
final url = WebDAVService.instance.getFileUrl(file.path);
|
|
||||||
final song = Song(
|
|
||||||
id: file.path,
|
|
||||||
title: file.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
|
||||||
artist: '未知艺术家',
|
|
||||||
url: url,
|
|
||||||
);
|
|
||||||
context.read<AudioService>().setQueue([song], startIndex: 0);
|
|
||||||
await PlaybackService().play(url, headers: headers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 设置队列并播放
|
|
||||||
final audioService = context.read<AudioService>();
|
|
||||||
audioService.setQueue(queue, startIndex: startIndex);
|
|
||||||
|
|
||||||
// 6. 播放(AudioService 内部已经调用了 PlaybackService,但为了确保认证头传递)
|
|
||||||
// 这里再显式调用一下,确保认证头正确
|
|
||||||
final targetUrl = queue[startIndex].url!;
|
|
||||||
await PlaybackService().play(targetUrl, headers: headers);
|
|
||||||
|
|
||||||
// 7. 更新 AudioService 的播放状态(确保 UI 同步)
|
|
||||||
// setQueue 已经调用了 _playCurrent(),所以这里不需要重复调用
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('正在播放: ${file.name}'),
|
content: Text('正在播放: ${_safeDecode(file.name)}'),
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
@@ -166,46 +113,21 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 全部播放(从第一首开始)
|
// 全部播放
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void _playAll() async {
|
void _playAll() async {
|
||||||
try {
|
try {
|
||||||
final musicFiles = _items.where((item) => !item.isDirectory).toList();
|
// ⭐ 全部播放使用 playAllFromWebDAV
|
||||||
|
await PlaybackController().playAllFromWebDAV(
|
||||||
if (musicFiles.isEmpty) {
|
directoryPath: _currentPath,
|
||||||
if (mounted) {
|
allItems: _items,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('当前目录没有音乐文件'),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final headers = await WebDAVService.instance.getAuthHeaders();
|
|
||||||
|
|
||||||
final queue = musicFiles.map((item) {
|
|
||||||
final itemUrl = WebDAVService.instance.getFileUrl(item.path);
|
|
||||||
return Song(
|
|
||||||
id: item.path,
|
|
||||||
title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
|
||||||
artist: '未知艺术家',
|
|
||||||
url: itemUrl,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final audioService = context.read<AudioService>();
|
|
||||||
audioService.setQueue(queue, startIndex: 0);
|
|
||||||
|
|
||||||
final targetUrl = queue[0].url!;
|
|
||||||
await PlaybackService().play(targetUrl, headers: headers);
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final musicCount = _items.where((item) => !item.isDirectory).length;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('开始播放全部 (${queue.length}首)'),
|
content: Text('开始播放全部 ($musicCount 首)'),
|
||||||
backgroundColor: const Color(0xFF4CAF50),
|
backgroundColor: const Color(0xFF4CAF50),
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
@@ -257,7 +179,6 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
// 全部播放按钮
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.playlist_play),
|
icon: const Icon(Icons.playlist_play),
|
||||||
onPressed: _playAll,
|
onPressed: _playAll,
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// lib/repositories/playlist_repository.dart
|
||||||
|
import 'dart:math';
|
||||||
|
import '../database/song_database.dart';
|
||||||
|
import '../models/playlist.dart';
|
||||||
|
import '../services/audio_service.dart';
|
||||||
|
|
||||||
|
class PlaylistRepository {
|
||||||
|
final SongDatabase _db = SongDatabase();
|
||||||
|
|
||||||
|
String _generateId() {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
final random = Random();
|
||||||
|
return String.fromCharCodes(
|
||||||
|
Iterable.generate(
|
||||||
|
16, (_) => chars.codeUnitAt(random.nextInt(chars.length))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 歌单操作
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Future<List<Playlist>> getAllPlaylists() async {
|
||||||
|
final data = await _db.getAllPlaylists();
|
||||||
|
return data.map((e) => Playlist.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Playlist?> getPlaylist(String id) async {
|
||||||
|
final data = await _db.getPlaylist(id);
|
||||||
|
if (data == null) return null;
|
||||||
|
return Playlist.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Playlist> createPlaylist(String name) async {
|
||||||
|
final playlist = Playlist(id: _generateId(), name: name);
|
||||||
|
await _db.insertPlaylist(playlist.toJson());
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePlaylistName(String id, String newName) async {
|
||||||
|
await _db.updatePlaylist(id, {
|
||||||
|
'name': newName,
|
||||||
|
'updated_at': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ 新增:更新播放模式
|
||||||
|
Future<void> updatePlaylistPlayMode(String id, PlayMode playMode) async {
|
||||||
|
await _db.updatePlaylist(id, {
|
||||||
|
// ⭐ 修改前:_playModeToString
|
||||||
|
// 'play_mode': _playModeToString(playMode),
|
||||||
|
|
||||||
|
// ⭐ 修改后:用公开静态方法
|
||||||
|
'play_mode': Playlist.playModeToString(playMode),
|
||||||
|
'updated_at': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePlaylist(String id) async {
|
||||||
|
await _db.deletePlaylist(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 歌单歌曲操作
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Future<List<String>> getPlaylistSongIds(String playlistId) async {
|
||||||
|
final data = await _db.getPlaylistSongs(playlistId);
|
||||||
|
return data.map((e) => e['song_id'] as String).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addSong(String playlistId, String songId) async {
|
||||||
|
await _db.addSongToPlaylist(playlistId, songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeSong(String playlistId, String songId) async {
|
||||||
|
await _db.removeSongFromPlaylist(playlistId, songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reorderSongs(
|
||||||
|
String playlistId, int oldIndex, int newIndex) async {
|
||||||
|
if (oldIndex == newIndex) return;
|
||||||
|
final songs = await _db.getPlaylistSongs(playlistId);
|
||||||
|
if (oldIndex < 0 ||
|
||||||
|
oldIndex >= songs.length ||
|
||||||
|
newIndex < 0 ||
|
||||||
|
newIndex >= songs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final items = songs.map((e) => e['song_id'] as String).toList();
|
||||||
|
final item = items.removeAt(oldIndex);
|
||||||
|
items.insert(newIndex, item);
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
await _db.removeSongFromPlaylist(playlistId, items[i]);
|
||||||
|
await _db.addSongToPlaylist(playlistId, items[i], orderIndex: i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearPlaylist(String playlistId) async {
|
||||||
|
await _db.clearPlaylist(playlistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 收藏操作
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Future<List<String>> getFavorites() async {
|
||||||
|
final data = await _db.getFavorites();
|
||||||
|
return data.map((e) => e['song_id'] as String).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isFavorite(String songId) async {
|
||||||
|
return await _db.isFavorite(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addFavorite(String songId) async {
|
||||||
|
await _db.addFavorite(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeFavorite(String songId) async {
|
||||||
|
await _db.removeFavorite(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleFavorite(String songId) async {
|
||||||
|
await _db.toggleFavorite(songId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart' as media_kit; // ⭐ 加别名
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'playback_service.dart';
|
import 'playback_service.dart';
|
||||||
import '../metadata/metadata_service.dart';
|
import '../metadata/metadata_service.dart';
|
||||||
import '../database/song_database.dart';
|
import '../database/song_database.dart';
|
||||||
import '../utils/artwork_helper.dart';
|
import '../utils/artwork_helper.dart';
|
||||||
|
import '../repositories/playlist_repository.dart';
|
||||||
|
import '../models/playlist.dart'; // ⭐ 你的 Playlist 模型
|
||||||
|
|
||||||
enum PlayMode {
|
enum PlayMode {
|
||||||
sequential,
|
sequential,
|
||||||
@@ -48,6 +50,9 @@ class AudioService extends ChangeNotifier {
|
|||||||
List<int> _shuffledIndices = [];
|
List<int> _shuffledIndices = [];
|
||||||
int _shuffledIndex = -1;
|
int _shuffledIndex = -1;
|
||||||
|
|
||||||
|
// ---- 当前播放的歌单 ID(用于模式同步) ----
|
||||||
|
String? _currentPlaylistId;
|
||||||
|
|
||||||
// ---- 高频进度 ----
|
// ---- 高频进度 ----
|
||||||
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
|
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
|
||||||
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
|
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
|
||||||
@@ -64,6 +69,10 @@ class AudioService extends ChangeNotifier {
|
|||||||
// ⭐ 播放代数:每次切歌递增,用于校验异步任务是否过期
|
// ⭐ 播放代数:每次切歌递增,用于校验异步任务是否过期
|
||||||
int _playbackGeneration = 0;
|
int _playbackGeneration = 0;
|
||||||
|
|
||||||
|
// ---- Repository ----
|
||||||
|
final SongDatabase _db = SongDatabase();
|
||||||
|
final PlaylistRepository _playlistRepo = PlaylistRepository();
|
||||||
|
|
||||||
// ---- Getter ----
|
// ---- Getter ----
|
||||||
Song? get currentSong => _currentSong;
|
Song? get currentSong => _currentSong;
|
||||||
bool get isPlaying => _isPlaying;
|
bool get isPlaying => _isPlaying;
|
||||||
@@ -71,6 +80,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
List<Song> get queue => List.unmodifiable(_queue);
|
List<Song> get queue => List.unmodifiable(_queue);
|
||||||
int get currentIndex => _currentIndex;
|
int get currentIndex => _currentIndex;
|
||||||
bool get hasQueue => _queue.isNotEmpty;
|
bool get hasQueue => _queue.isNotEmpty;
|
||||||
|
String? get currentPlaylistId => _currentPlaylistId;
|
||||||
|
|
||||||
Duration get position => positionNotifier.value;
|
Duration get position => positionNotifier.value;
|
||||||
Duration get duration => durationNotifier.value;
|
Duration get duration => durationNotifier.value;
|
||||||
@@ -91,6 +101,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
_onSongChanged = callback;
|
_onSongChanged = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 切换播放模式(同步更新歌单) ----
|
||||||
void togglePlayMode() {
|
void togglePlayMode() {
|
||||||
switch (_playMode) {
|
switch (_playMode) {
|
||||||
case PlayMode.sequential:
|
case PlayMode.sequential:
|
||||||
@@ -104,8 +115,14 @@ class AudioService extends ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
// ⭐ 如果有当前歌单,同步更新歌单的播放模式
|
||||||
|
if (_currentPlaylistId != null) {
|
||||||
|
_playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 设置播放队列 ----
|
||||||
void setQueue(List<Song> queue, {int startIndex = 0}) {
|
void setQueue(List<Song> queue, {int startIndex = 0}) {
|
||||||
if (queue.isEmpty) {
|
if (queue.isEmpty) {
|
||||||
_clearQueue();
|
_clearQueue();
|
||||||
@@ -131,10 +148,14 @@ class AudioService extends ChangeNotifier {
|
|||||||
_currentIndex = -1;
|
_currentIndex = -1;
|
||||||
_shuffledIndices.clear();
|
_shuffledIndices.clear();
|
||||||
_shuffledIndex = -1;
|
_shuffledIndex = -1;
|
||||||
|
_currentPlaylistId = null;
|
||||||
stopPlay();
|
stopPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 播放指定歌曲 ----
|
||||||
Future<void> playSong(Song song) async {
|
Future<void> playSong(Song song) async {
|
||||||
|
// 播放单曲时清除歌单上下文
|
||||||
|
_currentPlaylistId = null;
|
||||||
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
||||||
setQueue([song], startIndex: 0);
|
setQueue([song], startIndex: 0);
|
||||||
} else {
|
} else {
|
||||||
@@ -142,6 +163,66 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放歌单
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 播放整个歌单
|
||||||
|
Future<void> playPlaylist(String playlistId, {int startIndex = 0}) async {
|
||||||
|
_currentPlaylistId = playlistId;
|
||||||
|
|
||||||
|
// 1. 获取歌单的播放模式
|
||||||
|
final playlist = await _playlistRepo.getPlaylist(playlistId);
|
||||||
|
if (playlist != null) {
|
||||||
|
_playMode = playlist.playMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取歌单歌曲 ID 列表
|
||||||
|
final songIds = await _playlistRepo.getPlaylistSongIds(playlistId);
|
||||||
|
if (songIds.isEmpty) return;
|
||||||
|
|
||||||
|
// 3. 将 song_id 转换为 Song 对象
|
||||||
|
final songs = <Song>[];
|
||||||
|
for (final id in songIds) {
|
||||||
|
final dbSong = await _db.getSongByPath(id);
|
||||||
|
if (dbSong != null) {
|
||||||
|
songs.add(Song(
|
||||||
|
id: dbSong['remote_path'] as String,
|
||||||
|
title: dbSong['title'] as String? ?? '',
|
||||||
|
artist: dbSong['artist'] as String? ?? '未知艺术家',
|
||||||
|
url: dbSong['remote_path'] as String,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// 如果 songs 表中没有记录,用路径作为 fallback
|
||||||
|
songs.add(Song(
|
||||||
|
id: id,
|
||||||
|
title: id.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
||||||
|
artist: '未知艺术家',
|
||||||
|
url: id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songs.isNotEmpty) {
|
||||||
|
setQueue(songs, startIndex: startIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将当前播放队列保存为歌单
|
||||||
|
Future<Playlist> saveQueueAsPlaylist(String name) async {
|
||||||
|
// ⭐ 等待 createPlaylist 返回 Playlist 对象
|
||||||
|
final playlist = await _playlistRepo.createPlaylist(name);
|
||||||
|
for (int i = 0; i < _queue.length; i++) {
|
||||||
|
final song = _queue[i];
|
||||||
|
await _playlistRepo.addSong(playlist.id, song.id);
|
||||||
|
}
|
||||||
|
return playlist; // ⭐ 直接返回 Playlist 对象
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 播放核心
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void _playCurrent() {
|
void _playCurrent() {
|
||||||
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
|
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
|
||||||
stopPlay();
|
stopPlay();
|
||||||
@@ -177,9 +258,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
return raw.hashCode.toString();
|
return raw.hashCode.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ 核心:带 generation 校验的 metadata 加载
|
|
||||||
// lib/services/audio_service.dart
|
|
||||||
|
|
||||||
Future<void> _loadMetadataForCurrentSong(int generation) async {
|
Future<void> _loadMetadataForCurrentSong(int generation) async {
|
||||||
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
|
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
|
||||||
|
|
||||||
@@ -188,7 +266,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
final songId = song.id;
|
final songId = song.id;
|
||||||
|
|
||||||
if (song.url == null || song.url!.isEmpty) {
|
if (song.url == null || song.url!.isEmpty) {
|
||||||
// 没有 URL,直接推送 fallback
|
|
||||||
_onSongChanged?.call(song);
|
_onSongChanged?.call(song);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -200,7 +277,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
fileId: song.id,
|
fileId: song.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// generation 校验
|
|
||||||
if (_playbackGeneration != generation) {
|
if (_playbackGeneration != generation) {
|
||||||
debugPrint('⚠️ [AudioService] metadata stale (generation), ignoring');
|
debugPrint('⚠️ [AudioService] metadata stale (generation), ignoring');
|
||||||
return;
|
return;
|
||||||
@@ -215,7 +291,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 Song
|
|
||||||
final updatedSong = Song(
|
final updatedSong = Song(
|
||||||
id: song.id,
|
id: song.id,
|
||||||
title: metadata.title.isNotEmpty ? metadata.title : song.title,
|
title: metadata.title.isNotEmpty ? metadata.title : song.title,
|
||||||
@@ -227,18 +302,16 @@ class AudioService extends ChangeNotifier {
|
|||||||
_currentSong = updatedSong;
|
_currentSong = updatedSong;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// ⭐ 一次性推送完整数据(含 artwork)
|
|
||||||
_onSongChanged?.call(updatedSong);
|
_onSongChanged?.call(updatedSong);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [AudioService] metadata updated: ${updatedSong.title} - ${updatedSong.artist} (generation $generation)');
|
'✅ [AudioService] metadata updated: ${updatedSong.title} - ${updatedSong.artist} (generation $generation)');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ [AudioService] metadata load failed: $e, using fallback');
|
debugPrint('⚠️ [AudioService] metadata load failed: $e, using fallback');
|
||||||
// 异常时推送 fallback(无 artwork)
|
|
||||||
_onSongChanged?.call(song);
|
_onSongChanged?.call(song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清除当前歌曲的缓存(metadata + 封面图 + 文件缓存),并强制重新播放
|
/// 清除当前歌曲的缓存
|
||||||
Future<void> clearCurrentSongCache() async {
|
Future<void> clearCurrentSongCache() async {
|
||||||
if (_currentSong == null) return;
|
if (_currentSong == null) return;
|
||||||
final song = _currentSong!;
|
final song = _currentSong!;
|
||||||
@@ -294,6 +367,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 下一首 ----
|
||||||
void next() {
|
void next() {
|
||||||
if (_queue.isEmpty) return;
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
@@ -311,6 +385,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
_playCurrent();
|
_playCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 上一首 ----
|
||||||
void previous() {
|
void previous() {
|
||||||
if (_queue.isEmpty) return;
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
@@ -336,6 +411,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
_playCurrent();
|
_playCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 播放/暂停 ----
|
||||||
void togglePlay() {
|
void togglePlay() {
|
||||||
if (_currentSong == null) return;
|
if (_currentSong == null) return;
|
||||||
|
|
||||||
@@ -371,6 +447,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
_currentIndex = -1;
|
_currentIndex = -1;
|
||||||
_shuffledIndices.clear();
|
_shuffledIndices.clear();
|
||||||
_shuffledIndex = -1;
|
_shuffledIndex = -1;
|
||||||
|
_currentPlaylistId = null;
|
||||||
stopPlay();
|
stopPlay();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user