暂时把bug先处理掉了,准备尝试实现最小播放目标

This commit is contained in:
2026-08-15 23:20:54 +08:00
parent 902788c736
commit 699ab7ce86
11 changed files with 313 additions and 130 deletions
+21 -5
View File
@@ -1,7 +1,19 @@
import 'package:flutter/material.dart';
import '../models/song_model.dart';
// 这是一个 ChangeNotifier,以后用来刷新底部的迷你播放条
class Song {
final String id;
final String title;
final String artist;
final String? url;
Song({
required this.id,
required this.title,
required this.artist,
this.url,
});
}
class AudioService extends ChangeNotifier {
Song? _currentSong;
bool _isPlaying = false;
@@ -9,16 +21,20 @@ class AudioService extends ChangeNotifier {
Song? get currentSong => _currentSong;
bool get isPlaying => _isPlaying;
// 占位方法:以后这里会调用 media_kit
void playSong(Song song) {
_currentSong = song;
_isPlaying = true;
notifyListeners(); // 刷新 UI
print('🎵 准备播放: ${song.title} - ${song.artist}');
notifyListeners();
}
void togglePlay() {
_isPlaying = !_isPlaying;
notifyListeners();
}
void stopPlay() {
_currentSong = null;
_isPlaying = false;
notifyListeners();
}
}
+46 -8
View File
@@ -1,5 +1,4 @@
import 'package:media_kit/media_kit.dart';
import 'package:media_kit/media_kit.dart' as media_kit;
class PlaybackService {
static final PlaybackService _instance = PlaybackService._();
@@ -7,22 +6,61 @@ class PlaybackService {
PlaybackService._();
late final Player _player;
bool get isInitialized => _player.state.playing;
bool _initialized = false;
void init() {
if (_initialized) return;
_player = Player();
_initialized = true;
}
Future<void> play(String url) async {
if (!_initialized) init();
await _player.open(Media(url));
await _player.play();
}
void pause() => _player.pause();
void resume() => _player.play();
void stop() => _player.stop();
void dispose() => _player.dispose();
void pause() {
if (_initialized) {
_player.pause();
}
}
// 状态流
Stream<PlayerState> get stateStream => _player.stream;
void resume() {
if (_initialized) {
_player.play();
}
}
void stop() {
if (_initialized) {
_player.stop();
}
}
void dispose() {
if (_initialized) {
_player.dispose();
_initialized = false;
}
}
// ✅ 直接返回 PlayerStream(它就是播放器的状态流)
PlayerStream get stateStream {
if (!_initialized) init();
return _player.stream;
}
// 获取当前播放状态
PlayerState get currentState {
if (!_initialized) {
return PlayerState(
playing: false,
position: Duration.zero,
duration: Duration.zero,
buffering: false,
);
}
return _player.state;
}
}
+116 -29
View File
@@ -1,5 +1,7 @@
import 'package:webdav_client/webdav_client.dart';
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:xml/xml.dart';
class WebDAVService {
static const String _keyBaseUrl = 'webdav_base_url';
@@ -11,10 +13,13 @@ class WebDAVService {
WebDAVService._();
WebDAVClient? _client;
Dio? _dio;
String? _baseUrl;
String? _username;
bool get isConnected => _client != null;
bool get isConnected => _dio != null;
String? get baseUrl => _baseUrl;
String? get username => _username;
// 保存凭据
Future<void> saveCredentials(
@@ -23,11 +28,16 @@ class WebDAVService {
await prefs.setString(_keyBaseUrl, baseUrl);
await prefs.setString(_keyUsername, username);
await prefs.setString(_keyPassword, password);
_baseUrl = baseUrl;
_client = WebDAVClient(
baseUri: Uri.parse(baseUrl),
credentials: '$username:$password',
);
_username = username;
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
headers: {
'Authorization':
'Basic ${base64Encode(utf8.encode('$username:$password'))}',
},
));
}
// 加载已保存的凭据
@@ -36,12 +46,17 @@ class WebDAVService {
final baseUrl = prefs.getString(_keyBaseUrl);
final username = prefs.getString(_keyUsername);
final password = prefs.getString(_keyPassword);
if (baseUrl != null && username != null && password != null) {
_baseUrl = baseUrl;
_client = WebDAVClient(
baseUri: Uri.parse(baseUrl),
credentials: '$username:$password',
);
_username = username;
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
headers: {
'Authorization':
'Basic ${base64Encode(utf8.encode('$username:$password'))}',
},
));
return true;
}
return false;
@@ -53,36 +68,108 @@ class WebDAVService {
await prefs.remove(_keyBaseUrl);
await prefs.remove(_keyUsername);
await prefs.remove(_keyPassword);
_client = null;
_dio = null;
_baseUrl = null;
_username = null;
}
// 获取音乐文件列表(仅 .mp3 .flac .m4a .ape .wav
// 获取音乐文件列表(通过 PROPFIND
Future<List<WebDAVFileItem>> getMusicFiles({String path = '/'}) async {
if (_client == null) throw Exception('WebDAV 未连接');
if (_dio == null) throw Exception('WebDAV 未连接');
final items = await _client!.listAll(recursive: true);
// PROPFIND 请求体(Depth: 1 表示获取子项)
final body = '''
<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
<prop>
<resourcetype/>
<getcontentlength/>
<getlastmodified/>
</prop>
</propfind>
''';
final response = await _dio!.request(
path,
options: Options(
method: 'PROPFIND',
headers: {
'Depth': '1',
'Content-Type': 'application/xml; charset=utf-8',
},
),
data: body,
);
if (response.statusCode != 207) {
throw Exception('WebDAV 响应异常: ${response.statusCode}');
}
final xml = XmlDocument.parse(response.data as String);
final musicExtensions = ['.mp3', '.flac', '.m4a', '.ape', '.wav', '.opus'];
return items
.where((item) =>
item.isFile &&
musicExtensions.any((ext) => item.path.toLowerCase().endsWith(ext)))
.map((item) => WebDAVFileItem(
path: item.path,
name: item.path.split('/').last,
size: item.size,
modified: item.modified,
))
.toList();
final items = <WebDAVFileItem>[];
// 查找所有响应项
for (final responseNode in xml.findAllElements('response')) {
final hrefNode = responseNode.findElements('href').firstOrNull;
if (hrefNode == null) continue;
String fullPath = Uri.decodeComponent(hrefNode.text.trim());
// 去掉 baseUrl 前缀,得到相对路径
String relativePath = fullPath;
if (_baseUrl != null) {
final baseUri = Uri.parse(_baseUrl!);
final fullUri = Uri.parse(fullPath);
relativePath = fullUri.path;
if (baseUri.path.isNotEmpty && relativePath.startsWith(baseUri.path)) {
relativePath = relativePath.substring(baseUri.path.length);
}
}
if (relativePath.isEmpty || relativePath == '/') continue;
// 检查是否是文件(非集合)
final resTypeNode = responseNode.findElements('resourcetype').firstOrNull;
final isCollection =
resTypeNode?.findElements('collection').isNotEmpty ?? false;
if (isCollection) continue;
// 检查扩展名
final fileName = relativePath.split('/').last;
if (!musicExtensions.any((ext) => fileName.toLowerCase().endsWith(ext)))
continue;
// 获取文件大小和修改时间
int? size;
DateTime? modified;
final sizeNode =
responseNode.findElements('getcontentlength').firstOrNull;
if (sizeNode != null) {
size = int.tryParse(sizeNode.text);
}
final modifiedNode =
responseNode.findElements('getlastmodified').firstOrNull;
if (modifiedNode != null) {
try {
modified = DateTime.parse(modifiedNode.text);
} catch (_) {}
}
items.add(WebDAVFileItem(
path: relativePath,
name: fileName,
size: size,
modified: modified,
));
}
return items;
}
// 获取文件的完整下载 URL(用于播放)
// 获取文件的完整 URL
String getFileUrl(String path) {
if (_baseUrl == null) throw Exception('WebDAV 未配置');
// 确保 baseUrl 末尾有 '/'
final base = _baseUrl!.endsWith('/') ? _baseUrl! : '$_baseUrl/';
// 去掉路径开头的 '/'
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
return '$base$cleanPath';
}