现在正在修复首次启动点击跳曲问题与顺序播放的跳曲问题
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
// lib/controllers/playback_controller.dart
|
// lib/controllers/playback_controller.dart
|
||||||
|
import 'package:flutter/foundation.dart'; // ⭐ 添加
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
import '../services/webdav_service.dart';
|
import '../services/webdav_service.dart';
|
||||||
import '../database/song_database.dart';
|
import '../database/song_database.dart';
|
||||||
@@ -12,27 +13,43 @@ class PlaybackController {
|
|||||||
final SongDatabase _db = SongDatabase();
|
final SongDatabase _db = SongDatabase();
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 播放 WebDAV 目录中的歌曲(从点击歌曲开始)
|
// 从 WebDAV 目录播放(点击歌曲)
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> playFromWebDAV({
|
Future<void> playFromWebDAV({
|
||||||
required String directoryPath,
|
required String directoryPath,
|
||||||
required String clickedSongPath,
|
required String clickedSongPath,
|
||||||
required List<WebDAVItem> allItems,
|
required List<WebDAVItem> allItems,
|
||||||
}) async {
|
}) async {
|
||||||
final musicFiles = allItems.where((item) => !item.isDirectory).toList();
|
// 1. 过滤出所有音乐文件(去掉目录)
|
||||||
|
final musicFiles = allItems.where((item) => !item.isDirectory).toList()
|
||||||
|
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
if (musicFiles.isEmpty) return;
|
if (musicFiles.isEmpty) return;
|
||||||
|
|
||||||
|
// 2. 调试信息
|
||||||
|
debugPrint('🎯 [PlaybackController] musicFiles:');
|
||||||
|
for (int i = 0; i < musicFiles.length; i++) {
|
||||||
|
debugPrint(' $i: ${musicFiles[i].path}');
|
||||||
|
}
|
||||||
|
debugPrint('🎯 [PlaybackController] clickedSongPath: $clickedSongPath');
|
||||||
|
|
||||||
|
// 3. 找到点击歌曲在音乐文件列表中的位置
|
||||||
final clickedIndex =
|
final clickedIndex =
|
||||||
musicFiles.indexWhere((item) => item.path == clickedSongPath);
|
musicFiles.indexWhere((item) => item.path == clickedSongPath);
|
||||||
|
debugPrint('🎯 [PlaybackController] clickedIndex: $clickedIndex');
|
||||||
|
|
||||||
if (clickedIndex == -1) {
|
if (clickedIndex == -1) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [PlaybackController] clicked song not found, fallback to single play');
|
||||||
await _playSingleSong(clickedSongPath);
|
await _playSingleSong(clickedSongPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 构建 Song 队列
|
||||||
final queue = await _buildSongQueue(musicFiles);
|
final queue = await _buildSongQueue(musicFiles);
|
||||||
|
|
||||||
|
// 5. 应用播放模式
|
||||||
final playMode = _audioService.playMode;
|
final playMode = _audioService.playMode;
|
||||||
// ⭐ 修复:显式指定类型
|
|
||||||
List<Song> finalQueue = List<Song>.from(queue);
|
List<Song> finalQueue = List<Song>.from(queue);
|
||||||
int startIndex = clickedIndex;
|
int startIndex = clickedIndex;
|
||||||
|
|
||||||
@@ -43,23 +60,27 @@ class PlaybackController {
|
|||||||
finalQueue = shuffled;
|
finalQueue = shuffled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'🎯 [PlaybackController] finalQueue length: ${finalQueue.length}, startIndex: $startIndex');
|
||||||
|
|
||||||
_audioService.setQueue(finalQueue, startIndex: startIndex);
|
_audioService.setQueue(finalQueue, startIndex: startIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 播放整个目录(从第一首开始)
|
// 从 WebDAV 目录播放(全部播放)
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> playAllFromWebDAV({
|
Future<void> playAllFromWebDAV({
|
||||||
required String directoryPath,
|
required String directoryPath,
|
||||||
required List<WebDAVItem> allItems,
|
required List<WebDAVItem> allItems,
|
||||||
}) async {
|
}) async {
|
||||||
final musicFiles = allItems.where((item) => !item.isDirectory).toList();
|
final musicFiles = allItems.where((item) => !item.isDirectory).toList()
|
||||||
|
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
if (musicFiles.isEmpty) return;
|
if (musicFiles.isEmpty) return;
|
||||||
|
|
||||||
final queue = await _buildSongQueue(musicFiles);
|
final queue = await _buildSongQueue(musicFiles);
|
||||||
|
|
||||||
final playMode = _audioService.playMode;
|
final playMode = _audioService.playMode;
|
||||||
// ⭐ 修复:显式指定类型
|
|
||||||
List<Song> finalQueue = List<Song>.from(queue);
|
List<Song> finalQueue = List<Song>.from(queue);
|
||||||
if (playMode == PlayMode.shuffle) {
|
if (playMode == PlayMode.shuffle) {
|
||||||
finalQueue = List<Song>.from(queue)..shuffle();
|
finalQueue = List<Song>.from(queue)..shuffle();
|
||||||
@@ -69,9 +90,10 @@ class PlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 播放单曲(fallback)
|
// 单曲播放(fallback)
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> _playSingleSong(String songPath) async {
|
Future<void> _playSingleSong(String songPath) async {
|
||||||
|
debugPrint('🎯 [PlaybackController] _playSingleSong: $songPath');
|
||||||
final url = WebDAVService.instance.getFileUrl(songPath);
|
final url = WebDAVService.instance.getFileUrl(songPath);
|
||||||
final dbSong = await _db.getSongByPath(songPath);
|
final dbSong = await _db.getSongByPath(songPath);
|
||||||
final song = Song(
|
final song = Song(
|
||||||
@@ -85,7 +107,7 @@ class PlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 辅助方法:从音乐文件列表构建 Song 队列
|
// 构建 Song 队列
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<List<Song>> _buildSongQueue(List<WebDAVItem> musicFiles) async {
|
Future<List<Song>> _buildSongQueue(List<WebDAVItem> musicFiles) async {
|
||||||
final queue = <Song>[];
|
final queue = <Song>[];
|
||||||
@@ -113,19 +135,10 @@ class PlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 播放控制(透传到 AudioService)
|
// 播放控制透传
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
void next() {
|
void next() => _audioService.next();
|
||||||
_audioService.next();
|
void previous() => _audioService.previous();
|
||||||
}
|
void togglePlayMode() => _audioService.togglePlayMode();
|
||||||
|
|
||||||
void previous() {
|
|
||||||
_audioService.previous();
|
|
||||||
}
|
|
||||||
|
|
||||||
void togglePlayMode() {
|
|
||||||
_audioService.togglePlayMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayMode get playMode => _audioService.playMode;
|
PlayMode get playMode => _audioService.playMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// lib/pages/webdav_file_list_page.dart
|
// lib/pages/webdav_file_list_page.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import '../services/webdav_service.dart';
|
import '../services/webdav_service.dart';
|
||||||
import '../services/playback_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';
|
import '../controllers/playback_controller.dart';
|
||||||
@@ -79,12 +76,11 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 播放单首(点击歌曲)
|
// ⭐ 播放单首(使用 PlaybackController)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void _playSong(WebDAVItem file) async {
|
void _playSong(WebDAVItem file) async {
|
||||||
try {
|
try {
|
||||||
// ⭐ 直接使用 PlaybackController 单例
|
|
||||||
await PlaybackController().playFromWebDAV(
|
await PlaybackController().playFromWebDAV(
|
||||||
directoryPath: _currentPath,
|
directoryPath: _currentPath,
|
||||||
clickedSongPath: file.path,
|
clickedSongPath: file.path,
|
||||||
@@ -112,12 +108,11 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 全部播放
|
// ⭐ 全部播放(使用 PlaybackController)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void _playAll() async {
|
void _playAll() async {
|
||||||
try {
|
try {
|
||||||
// ⭐ 全部播放使用 playAllFromWebDAV
|
|
||||||
await PlaybackController().playAllFromWebDAV(
|
await PlaybackController().playAllFromWebDAV(
|
||||||
directoryPath: _currentPath,
|
directoryPath: _currentPath,
|
||||||
allItems: _items,
|
allItems: _items,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
// lib/services/app_lifecycle_manager.dart
|
// lib/services/app_lifecycle_manager.dart
|
||||||
import 'dart:async';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
|
|
||||||
/// App 生命周期状态(扩展了 Flutter 原生状态)
|
/// App 生命周期状态(扩展了 Flutter 原生状态)
|
||||||
enum AppLifecycleStatus {
|
enum AppLifecycleStatus {
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ 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' as media_kit; // ⭐ 加别名
|
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 '../repositories/playlist_repository.dart';
|
||||||
import '../models/playlist.dart'; // ⭐ 你的 Playlist 模型
|
import '../models/playlist.dart';
|
||||||
|
import 'webdav_service.dart';
|
||||||
|
|
||||||
enum PlayMode {
|
enum PlayMode {
|
||||||
sequential,
|
sequential,
|
||||||
@@ -116,7 +117,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// ⭐ 如果有当前歌单,同步更新歌单的播放模式
|
|
||||||
if (_currentPlaylistId != null) {
|
if (_currentPlaylistId != null) {
|
||||||
_playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode);
|
_playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode);
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
|
|
||||||
// ---- 播放指定歌曲 ----
|
// ---- 播放指定歌曲 ----
|
||||||
Future<void> playSong(Song song) async {
|
Future<void> playSong(Song song) async {
|
||||||
// 播放单曲时清除歌单上下文
|
|
||||||
_currentPlaylistId = null;
|
_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);
|
||||||
@@ -167,21 +166,17 @@ class AudioService extends ChangeNotifier {
|
|||||||
// 播放歌单
|
// 播放歌单
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/// 播放整个歌单
|
|
||||||
Future<void> playPlaylist(String playlistId, {int startIndex = 0}) async {
|
Future<void> playPlaylist(String playlistId, {int startIndex = 0}) async {
|
||||||
_currentPlaylistId = playlistId;
|
_currentPlaylistId = playlistId;
|
||||||
|
|
||||||
// 1. 获取歌单的播放模式
|
|
||||||
final playlist = await _playlistRepo.getPlaylist(playlistId);
|
final playlist = await _playlistRepo.getPlaylist(playlistId);
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
_playMode = playlist.playMode;
|
_playMode = playlist.playMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取歌单歌曲 ID 列表
|
|
||||||
final songIds = await _playlistRepo.getPlaylistSongIds(playlistId);
|
final songIds = await _playlistRepo.getPlaylistSongIds(playlistId);
|
||||||
if (songIds.isEmpty) return;
|
if (songIds.isEmpty) return;
|
||||||
|
|
||||||
// 3. 将 song_id 转换为 Song 对象
|
|
||||||
final songs = <Song>[];
|
final songs = <Song>[];
|
||||||
for (final id in songIds) {
|
for (final id in songIds) {
|
||||||
final dbSong = await _db.getSongByPath(id);
|
final dbSong = await _db.getSongByPath(id);
|
||||||
@@ -193,7 +188,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
url: dbSong['remote_path'] as String,
|
url: dbSong['remote_path'] as String,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// 如果 songs 表中没有记录,用路径作为 fallback
|
|
||||||
songs.add(Song(
|
songs.add(Song(
|
||||||
id: id,
|
id: id,
|
||||||
title: id.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
title: id.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
||||||
@@ -208,19 +202,17 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将当前播放队列保存为歌单
|
|
||||||
Future<Playlist> saveQueueAsPlaylist(String name) async {
|
Future<Playlist> saveQueueAsPlaylist(String name) async {
|
||||||
// ⭐ 等待 createPlaylist 返回 Playlist 对象
|
|
||||||
final playlist = await _playlistRepo.createPlaylist(name);
|
final playlist = await _playlistRepo.createPlaylist(name);
|
||||||
for (int i = 0; i < _queue.length; i++) {
|
for (int i = 0; i < _queue.length; i++) {
|
||||||
final song = _queue[i];
|
final song = _queue[i];
|
||||||
await _playlistRepo.addSong(playlist.id, song.id);
|
await _playlistRepo.addSong(playlist.id, song.id);
|
||||||
}
|
}
|
||||||
return playlist; // ⭐ 直接返回 Playlist 对象
|
return playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 播放核心
|
// ⭐ 播放核心(修复:自动添加认证头)
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void _playCurrent() {
|
void _playCurrent() {
|
||||||
@@ -242,17 +234,37 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackService().play(song.url!);
|
// ⭐ 关键修复:播放时自动添加认证头
|
||||||
|
_playWithHeaders(song.url!);
|
||||||
|
|
||||||
_syncPlayerStateDelayed();
|
_syncPlayerStateDelayed();
|
||||||
|
|
||||||
// ⭐ 立即推送基本信息(无 artwork)
|
|
||||||
_onSongChanged?.call(song);
|
_onSongChanged?.call(song);
|
||||||
|
|
||||||
// ⭐ 异步加载完整 metadata(含 artwork)
|
|
||||||
_loadMetadataForCurrentSong(generation);
|
_loadMetadataForCurrentSong(generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ⭐ 播放带认证头的 URL
|
||||||
|
Future<void> _playWithHeaders(String url) async {
|
||||||
|
final headers = await _getAuthHeadersForUrl(url);
|
||||||
|
await PlaybackService().play(url, headers: headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ⭐ 获取 URL 对应的认证头(WebDAV 需要,本地文件不需要)
|
||||||
|
Future<Map<String, String>> _getAuthHeadersForUrl(String url) async {
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
try {
|
||||||
|
final headers = await WebDAVService.instance.getAuthHeaders();
|
||||||
|
if (headers.isNotEmpty) {
|
||||||
|
debugPrint('🔑 [AudioService] 已添加认证头到播放请求');
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('⚠️ [AudioService] 获取认证头失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
String _generateSongKey(String url, int fileSize, int modifiedTime) {
|
String _generateSongKey(String url, int fileSize, int modifiedTime) {
|
||||||
final raw = '$url|$fileSize|$modifiedTime';
|
final raw = '$url|$fileSize|$modifiedTime';
|
||||||
return raw.hashCode.toString();
|
return raw.hashCode.toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user