引入播放列表库,同时引入全新的播放控制器,以控制读库与播放模式的动作,但是功能仍在完善,需要确认部分边界问题

This commit is contained in:
2026-08-27 22:29:20 +08:00
parent ad6617592e
commit e58be9c17f
6 changed files with 677 additions and 126 deletions
+131
View File
@@ -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
View File
@@ -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,
+69
View File
@@ -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;
}
}
}
+17 -96
View File
@@ -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,
+127
View File
@@ -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);
}
}
+87 -10
View File
@@ -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();
} }