优化部分功能逻辑:元数据信息漂移
优化部分体验问题,包含动画、界面层级等
This commit is contained in:
+99
-73
@@ -1,3 +1,4 @@
|
|||||||
|
// lib/main.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
@@ -49,10 +50,10 @@ void main() async {
|
|||||||
debugPrint('❌ MediaKit 初始化失败: $e');
|
debugPrint('❌ MediaKit 初始化失败: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ 创建 Handler(AudioService 稍后初始化)
|
// ⭐ 创建 Handler
|
||||||
_audioHandler = AudioPlayerHandler();
|
_audioHandler = AudioPlayerHandler();
|
||||||
|
|
||||||
// ⭐ 注册切歌回调:当歌曲切换时,立即更新通知
|
// ⭐ 注册切歌回调
|
||||||
AudioService().setOnSongChanged((song) {
|
AudioService().setOnSongChanged((song) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📢 [main] song.artwork is ${song.artwork != null ? 'not null' : 'null'}');
|
'📢 [main] song.artwork is ${song.artwork != null ? 'not null' : 'null'}');
|
||||||
@@ -60,7 +61,7 @@ void main() async {
|
|||||||
id: song.id,
|
id: song.id,
|
||||||
title: song.title,
|
title: song.title,
|
||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
artwork: song.artwork, // ⭐ 传递封面图
|
artwork: song.artwork,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,7 +70,6 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => AudioService()),
|
ChangeNotifierProvider(create: (_) => AudioService()),
|
||||||
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
||||||
// ⭐ 生命周期管理器
|
|
||||||
ChangeNotifierProvider<AppLifecycleManager>(
|
ChangeNotifierProvider<AppLifecycleManager>(
|
||||||
create: (_) => lifecycleManager,
|
create: (_) => lifecycleManager,
|
||||||
),
|
),
|
||||||
@@ -108,9 +108,18 @@ class RouteManager extends ChangeNotifier {
|
|||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 导航观察者(监听路由变化)
|
// 导航观察者(监听路由变化)
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// lib/main.dart
|
||||||
|
|
||||||
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
||||||
void _updateRoute(Route? route) {
|
void _updateRoute(Route? route) {
|
||||||
final name = route?.settings.name ?? '/';
|
if (route == null) return;
|
||||||
|
|
||||||
|
// ⭐ 忽略 PopupRoute(菜单、对话框等)
|
||||||
|
if (route is PopupRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = route.settings.name ?? '/';
|
||||||
RouteManager().updateRoute(name);
|
RouteManager().updateRoute(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,12 +130,21 @@ class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didPop(Route route, Route? previousRoute) {
|
void didPop(Route route, Route? previousRoute) {
|
||||||
_updateRoute(previousRoute);
|
// ⭐ 返回时,使用上一个路由(previousRoute)
|
||||||
|
if (previousRoute != null && previousRoute is! PopupRoute) {
|
||||||
|
final name = previousRoute.settings.name ?? '/';
|
||||||
|
RouteManager().updateRoute(name);
|
||||||
|
} else if (previousRoute == null) {
|
||||||
|
RouteManager().updateRoute('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didReplace({Route? newRoute, Route? oldRoute}) {
|
void didReplace({Route? newRoute, Route? oldRoute}) {
|
||||||
_updateRoute(newRoute);
|
if (newRoute != null && newRoute is! PopupRoute) {
|
||||||
|
final name = newRoute.settings.name ?? '/';
|
||||||
|
RouteManager().updateRoute(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,49 +159,31 @@ class QTPlayerApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _QTPlayerAppState extends State<QTPlayerApp> {
|
class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||||
// ⭐ build 计数器(诊断用)
|
|
||||||
static int _buildCount = 0;
|
static int _buildCount = 0;
|
||||||
|
|
||||||
bool _isInitializing = false;
|
bool _isInitializing = false;
|
||||||
|
|
||||||
// ⭐ 保存生命周期监听器引用
|
|
||||||
void Function(AppLifecycleStatus)? _lifecycleListener;
|
void Function(AppLifecycleStatus)? _lifecycleListener;
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 生命周期
|
|
||||||
// ============================================================
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
debugPrint('⏱️ T1.5 initState: ${stopwatch.elapsedMilliseconds}ms');
|
debugPrint('⏱️ T1.5 initState: ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
debugPrint('⏱️ T2 首帧回调: ${stopwatch.elapsedMilliseconds}ms');
|
debugPrint('⏱️ T2 首帧回调: ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
// ⭐ 1. 初始化生命周期管理器
|
|
||||||
lifecycleManager.init();
|
lifecycleManager.init();
|
||||||
|
|
||||||
// ⭐ 2. 绑定生命周期到播放器
|
|
||||||
_bindLifecycleToPlayback();
|
_bindLifecycleToPlayback();
|
||||||
|
|
||||||
// ⭐ 3. 后台初始化
|
|
||||||
_startBackgroundInitialization();
|
_startBackgroundInitialization();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// ⭐ 移除生命周期监听器
|
|
||||||
if (_lifecycleListener != null) {
|
if (_lifecycleListener != null) {
|
||||||
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 生命周期 → 播放器绑定
|
|
||||||
// ============================================================
|
|
||||||
void _bindLifecycleToPlayback() {
|
void _bindLifecycleToPlayback() {
|
||||||
if (_lifecycleListener != null) {
|
if (_lifecycleListener != null) {
|
||||||
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
||||||
@@ -195,15 +195,12 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
case AppLifecycleStatus.inactive:
|
case AppLifecycleStatus.inactive:
|
||||||
debugPrint('🎵 App 进入后台,播放继续');
|
debugPrint('🎵 App 进入后台,播放继续');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleStatus.resumed:
|
case AppLifecycleStatus.resumed:
|
||||||
debugPrint('🎵 App 回到前台');
|
debugPrint('🎵 App 回到前台');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleStatus.backgroundIdle:
|
case AppLifecycleStatus.backgroundIdle:
|
||||||
debugPrint('🎵 长后台唤醒,检查播放状态');
|
debugPrint('🎵 长后台唤醒,检查播放状态');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -212,9 +209,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
lifecycleManager.addListenerToManager(_lifecycleListener!);
|
lifecycleManager.addListenerToManager(_lifecycleListener!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 后台初始化(精细时间切片)
|
|
||||||
// ============================================================
|
|
||||||
Future<void> _startBackgroundInitialization() async {
|
Future<void> _startBackgroundInitialization() async {
|
||||||
if (_isInitializing) return;
|
if (_isInitializing) return;
|
||||||
_isInitializing = true;
|
_isInitializing = true;
|
||||||
@@ -222,7 +216,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
final tStart = DateTime.now();
|
final tStart = DateTime.now();
|
||||||
debugPrint('⏱️ T3 后台初始化开始');
|
debugPrint('⏱️ T3 后台初始化开始');
|
||||||
|
|
||||||
// ----- 1. PlaybackService -----
|
|
||||||
final t1 = DateTime.now();
|
final t1 = DateTime.now();
|
||||||
try {
|
try {
|
||||||
PlaybackService().init();
|
PlaybackService().init();
|
||||||
@@ -231,7 +224,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
debugPrint('❌ PlaybackService 失败: $e');
|
debugPrint('❌ PlaybackService 失败: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- 2. WebDAV(只加载凭证) -----
|
|
||||||
final t2 = DateTime.now();
|
final t2 = DateTime.now();
|
||||||
try {
|
try {
|
||||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||||
@@ -240,10 +232,8 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
debugPrint('❌ WebDAV 凭证加载失败: $e');
|
debugPrint('❌ WebDAV 凭证加载失败: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- 3. ⭐ AudioService(后台播放核心) -----
|
|
||||||
await _initAudioService();
|
await _initAudioService();
|
||||||
|
|
||||||
// ----- 4. 通知权限 -----
|
|
||||||
final t3 = DateTime.now();
|
final t3 = DateTime.now();
|
||||||
try {
|
try {
|
||||||
if (await Permission.notification.isDenied) {
|
if (await Permission.notification.isDenied) {
|
||||||
@@ -260,9 +250,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
debugPrint('⏱️ T4 全部初始化完成: ${tEnd.difference(tStart).inMilliseconds}ms');
|
debugPrint('⏱️ T4 全部初始化完成: ${tEnd.difference(tStart).inMilliseconds}ms');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// ⭐ AudioService 初始化(后台播放核心)
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
Future<void> _initAudioService() async {
|
Future<void> _initAudioService() async {
|
||||||
try {
|
try {
|
||||||
await audio_service.AudioService.init(
|
await audio_service.AudioService.init(
|
||||||
@@ -280,42 +267,61 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// 辅助方法
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
String _elapsedMs(DateTime start) {
|
String _elapsedMs(DateTime start) {
|
||||||
return '${DateTime.now().difference(start).inMilliseconds}';
|
return '${DateTime.now().difference(start).inMilliseconds}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ⭐ 路由生成(播放页滑入动画,速度 200ms)
|
||||||
// Build
|
// lib/main.dart - _onGenerateRoute 方法
|
||||||
// ════════════════════════════════════════════════════════════
|
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
|
||||||
|
if (settings.name == '/player') {
|
||||||
|
return PageRouteBuilder(
|
||||||
|
settings: settings,
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
|
return const PlayerPage();
|
||||||
|
},
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
// ⭐ 应用贝塞尔曲线
|
||||||
|
final curvedAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOutCubic, // 平滑减速
|
||||||
|
// curve: Curves.easeOutBack, // 轻微回弹,更灵动
|
||||||
|
// curve: Curves.fastOutSlowIn, // Material 风格
|
||||||
|
);
|
||||||
|
const begin = Offset(0.0, 1.0);
|
||||||
|
const end = Offset.zero;
|
||||||
|
final tween = Tween(begin: begin, end: end);
|
||||||
|
final offsetAnimation = tween.animate(curvedAnimation);
|
||||||
|
return SlideTransition(
|
||||||
|
position: offsetAnimation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
transitionDuration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialPageRoute(
|
||||||
|
settings: settings,
|
||||||
|
builder: (context) {
|
||||||
|
switch (settings.name) {
|
||||||
|
case '/':
|
||||||
|
return const HomePage();
|
||||||
|
case '/playlist':
|
||||||
|
return const PlaylistPage();
|
||||||
|
default:
|
||||||
|
return const HomePage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_buildCount++;
|
_buildCount++;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
// 监听歌曲变化,更新通知栏
|
|
||||||
// main.dart 中 build 方法里的这部分
|
|
||||||
final audioService = context.watch<AudioService>();
|
|
||||||
final handler = context.read<AudioPlayerHandler>();
|
|
||||||
final song = audioService.currentSong;
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (song != null) {
|
|
||||||
// ⭐ song 不为空时才调用
|
|
||||||
handler.updateNotification(
|
|
||||||
id: song.id,
|
|
||||||
title: song.title,
|
|
||||||
artist: song.artist,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 清空通知
|
|
||||||
handler.mediaItem.add(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: '清听',
|
title: '清听',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
@@ -331,11 +337,7 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
),
|
),
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
navigatorObservers: [MiniPlayerNavigatorObserver()],
|
navigatorObservers: [MiniPlayerNavigatorObserver()],
|
||||||
routes: {
|
onGenerateRoute: _onGenerateRoute,
|
||||||
'/': (context) => const HomePage(),
|
|
||||||
'/player': (context) => const PlayerPage(),
|
|
||||||
'/playlist': (context) => const PlaylistPage(),
|
|
||||||
},
|
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
@@ -344,12 +346,36 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
AnimatedBuilder(
|
AnimatedBuilder(
|
||||||
animation: RouteManager(),
|
animation: RouteManager(),
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
if (!RouteManager().showMiniPlayer) {
|
final routeManager = RouteManager();
|
||||||
return const SizedBox.shrink();
|
final shouldShow = routeManager.currentRoute != '/player' &&
|
||||||
}
|
routeManager.currentRoute != '/playlist';
|
||||||
return const Align(
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
switchInCurve: Curves.easeOut,
|
||||||
|
switchOutCurve: Curves.easeIn,
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
// ⭐ 淡入淡出 + 轻微上滑
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: shouldShow
|
||||||
|
? const Align(
|
||||||
|
key: ValueKey('mini_player_visible'),
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: GlobalMiniPlayer(),
|
child: GlobalMiniPlayer(),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(
|
||||||
|
key: ValueKey('mini_player_hidden'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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: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';
|
||||||
@@ -60,6 +61,9 @@ class AudioService extends ChangeNotifier {
|
|||||||
|
|
||||||
bool _isUserSeeking = false;
|
bool _isUserSeeking = false;
|
||||||
|
|
||||||
|
// ⭐ 播放代数:每次切歌递增,用于校验异步任务是否过期
|
||||||
|
int _playbackGeneration = 0;
|
||||||
|
|
||||||
// ---- Getter ----
|
// ---- Getter ----
|
||||||
Song? get currentSong => _currentSong;
|
Song? get currentSong => _currentSong;
|
||||||
bool get isPlaying => _isPlaying;
|
bool get isPlaying => _isPlaying;
|
||||||
@@ -144,6 +148,9 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_playbackGeneration++;
|
||||||
|
final generation = _playbackGeneration;
|
||||||
|
|
||||||
final song = _queue[_currentIndex];
|
final song = _queue[_currentIndex];
|
||||||
_currentSong = song;
|
_currentSong = song;
|
||||||
|
|
||||||
@@ -158,7 +165,11 @@ class AudioService extends ChangeNotifier {
|
|||||||
|
|
||||||
_syncPlayerStateDelayed();
|
_syncPlayerStateDelayed();
|
||||||
|
|
||||||
_loadMetadataForCurrentSong();
|
// ⭐ 立即推送基本信息(无 artwork)
|
||||||
|
_onSongChanged?.call(song);
|
||||||
|
|
||||||
|
// ⭐ 异步加载完整 metadata(含 artwork)
|
||||||
|
_loadMetadataForCurrentSong(generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _generateSongKey(String url, int fileSize, int modifiedTime) {
|
String _generateSongKey(String url, int fileSize, int modifiedTime) {
|
||||||
@@ -166,10 +177,21 @@ class AudioService extends ChangeNotifier {
|
|||||||
return raw.hashCode.toString();
|
return raw.hashCode.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadMetadataForCurrentSong() async {
|
// ⭐ 核心:带 generation 校验的 metadata 加载
|
||||||
|
// lib/services/audio_service.dart
|
||||||
|
|
||||||
|
Future<void> _loadMetadataForCurrentSong(int generation) async {
|
||||||
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
|
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
|
||||||
final song = _queue[_currentIndex];
|
|
||||||
if (song.url == null || song.url!.isEmpty) return;
|
final currentIndex = _currentIndex;
|
||||||
|
final song = _queue[currentIndex];
|
||||||
|
final songId = song.id;
|
||||||
|
|
||||||
|
if (song.url == null || song.url!.isEmpty) {
|
||||||
|
// 没有 URL,直接推送 fallback
|
||||||
|
_onSongChanged?.call(song);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final metadata = await MetadataService().getMetadata(
|
final metadata = await MetadataService().getMetadata(
|
||||||
@@ -178,23 +200,41 @@ class AudioService extends ChangeNotifier {
|
|||||||
fileId: song.id,
|
fileId: song.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (metadata.isNotEmpty) {
|
// generation 校验
|
||||||
|
if (_playbackGeneration != generation) {
|
||||||
|
debugPrint('⚠️ [AudioService] metadata stale (generation), ignoring');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_currentIndex != currentIndex) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [AudioService] metadata stale (index changed), ignoring');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentIndex >= _queue.length || _queue[currentIndex].id != songId) {
|
||||||
|
debugPrint('⚠️ [AudioService] metadata stale (song changed), ignoring');
|
||||||
|
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,
|
||||||
artist: metadata.artist.isNotEmpty ? metadata.artist : song.artist,
|
artist: metadata.artist.isNotEmpty ? metadata.artist : song.artist,
|
||||||
url: song.url,
|
url: song.url,
|
||||||
artwork: metadata.artwork, // ✅ 已有
|
artwork: metadata.artwork,
|
||||||
);
|
);
|
||||||
_queue[_currentIndex] = updatedSong;
|
_queue[_currentIndex] = updatedSong;
|
||||||
_currentSong = updatedSong;
|
_currentSong = updatedSong;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// ⭐ 传递完整的 updatedSong(包含 artwork)
|
// ⭐ 一次性推送完整数据(含 artwork)
|
||||||
_onSongChanged?.call(updatedSong);
|
_onSongChanged?.call(updatedSong);
|
||||||
}
|
debugPrint(
|
||||||
|
'✅ [AudioService] metadata updated: ${updatedSong.title} - ${updatedSong.artist} (generation $generation)');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ [AudioService] metadata load failed: $e');
|
debugPrint('⚠️ [AudioService] metadata load failed: $e, using fallback');
|
||||||
|
// 异常时推送 fallback(无 artwork)
|
||||||
|
_onSongChanged?.call(song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +247,8 @@ class AudioService extends ChangeNotifier {
|
|||||||
|
|
||||||
debugPrint('🗑️ [AudioService] clearing cache for: ${song.title}');
|
debugPrint('🗑️ [AudioService] clearing cache for: ${song.title}');
|
||||||
|
|
||||||
// 1. 获取当前索引
|
|
||||||
final currentIndex = _currentIndex;
|
final currentIndex = _currentIndex;
|
||||||
|
|
||||||
// 2. 尝试获取文件信息生成 song_key
|
|
||||||
String songKey;
|
String songKey;
|
||||||
try {
|
try {
|
||||||
final provider = MetadataService().getProviderForUrl(url);
|
final provider = MetadataService().getProviderForUrl(url);
|
||||||
@@ -226,17 +264,14 @@ class AudioService extends ChangeNotifier {
|
|||||||
songKey = url.hashCode.toString();
|
songKey = url.hashCode.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 清除 SQLite 记录
|
|
||||||
final db = SongDatabase();
|
final db = SongDatabase();
|
||||||
await db.deleteSong(songKey);
|
await db.deleteSong(songKey);
|
||||||
await db.deleteCache(songKey);
|
await db.deleteCache(songKey);
|
||||||
debugPrint('🗑️ [AudioService] SQLite records deleted: $songKey');
|
debugPrint('🗑️ [AudioService] SQLite records deleted: $songKey');
|
||||||
|
|
||||||
// 4. 删除封面图
|
|
||||||
await ArtworkHelper.deleteArtwork(songKey);
|
await ArtworkHelper.deleteArtwork(songKey);
|
||||||
debugPrint('🗑️ [AudioService] artwork deleted');
|
debugPrint('🗑️ [AudioService] artwork deleted');
|
||||||
|
|
||||||
// 5. 删除 metadata 临时缓存文件
|
|
||||||
try {
|
try {
|
||||||
final cacheDir = await getTemporaryDirectory();
|
final cacheDir = await getTemporaryDirectory();
|
||||||
final cachePath = '${cacheDir.path}/metadata_${url.hashCode}.tmp';
|
final cachePath = '${cacheDir.path}/metadata_${url.hashCode}.tmp';
|
||||||
@@ -249,14 +284,11 @@ class AudioService extends ChangeNotifier {
|
|||||||
// 忽略
|
// 忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 清除内存缓存
|
|
||||||
await MetadataService().clearCache(song.id);
|
await MetadataService().clearCache(song.id);
|
||||||
debugPrint('🗑️ [AudioService] memory cache cleared');
|
debugPrint('🗑️ [AudioService] memory cache cleared');
|
||||||
|
|
||||||
// 7. 停止并重新播放
|
|
||||||
if (currentIndex >= 0 && currentIndex < _queue.length) {
|
if (currentIndex >= 0 && currentIndex < _queue.length) {
|
||||||
stopPlay();
|
stopPlay();
|
||||||
// 确保重新播放同一首歌
|
|
||||||
_playCurrent();
|
_playCurrent();
|
||||||
debugPrint('🔄 [AudioService] song reloaded');
|
debugPrint('🔄 [AudioService] song reloaded');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,19 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// ⭐ 使用 Selector 监听完整的 Song 对象(包括 artwork)
|
|
||||||
final song = context.select<AudioService, Song?>((s) => s.currentSong);
|
final song = context.select<AudioService, Song?>((s) => s.currentSong);
|
||||||
final isPlaying = context.select<AudioService, bool>((s) => s.isPlaying);
|
final isPlaying = context.select<AudioService, bool>((s) => s.isPlaying);
|
||||||
|
|
||||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
|
// 如果没有歌曲,不显示
|
||||||
|
if (song == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
// ⭐ 背景高度从 56 改为 60
|
// 背景条
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -28,6 +31,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
color: const Color(0xFF1A1F1E),
|
color: const Color(0xFF1A1F1E),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 主内容
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -37,58 +41,54 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
bottom: Radius.circular(16),
|
bottom: Radius.circular(16),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 60, // ⭐ 从 56 改为 60
|
height: 60,
|
||||||
color: const Color(0xFF1A1F1E),
|
color: const Color(0xFF1A1F1E),
|
||||||
|
child: ClipRRect(
|
||||||
|
// ⭐ 修复涟漪圆角问题
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (song != null) {
|
|
||||||
navigatorKey.currentState?.pushNamed('/player');
|
navigatorKey.currentState?.pushNamed('/player');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
highlightColor: Colors.white.withOpacity(0.05),
|
highlightColor: Colors.white.withOpacity(0.05),
|
||||||
splashColor: Colors.white.withOpacity(0.1),
|
splashColor: Colors.white.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
// ⭐ 封面图容器尺寸从 40 改为 44(适配 60px 高度)
|
// 封面图
|
||||||
Container(
|
Container(
|
||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
color: song != null
|
color: const Color(0xFF2A3332),
|
||||||
? const Color(0xFF2A3332)
|
image: song.artwork != null
|
||||||
: Colors.grey[800],
|
|
||||||
// ⭐ 如果 song.artwork 存在,显示封面图
|
|
||||||
image: song?.artwork != null
|
|
||||||
? DecorationImage(
|
? DecorationImage(
|
||||||
image: MemoryImage(song!.artwork!),
|
image: MemoryImage(song.artwork!),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
child: song?.artwork == null
|
child: song.artwork == null
|
||||||
? Icon(
|
? const Icon(
|
||||||
song != null
|
Icons.music_note,
|
||||||
? Icons.music_note
|
color: Colors.white38,
|
||||||
: Icons.music_off,
|
|
||||||
color: song != null
|
|
||||||
? Colors.white38
|
|
||||||
: Colors.grey[600],
|
|
||||||
size: 20,
|
size: 20,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
// 标题 + 艺术家
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
song != null ? song.title : '清听',
|
song.title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -99,12 +99,10 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
song != null ? song.artist : '未播放',
|
song.artist,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: song != null
|
color: Colors.grey[400],
|
||||||
? Colors.grey[400]
|
|
||||||
: Colors.grey[600],
|
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@@ -113,20 +111,18 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 播放/暂停按钮
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
song != null && isPlaying
|
isPlaying ? Icons.pause : Icons.play_arrow,
|
||||||
? Icons.pause
|
color: Colors.white,
|
||||||
: Icons.play_arrow,
|
|
||||||
color: song != null ? Colors.white : Colors.grey[600],
|
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (song != null) {
|
|
||||||
context.read<AudioService>().togglePlay();
|
context.read<AudioService>().togglePlay();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// 播放列表按钮
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.playlist_play_outlined,
|
Icons.playlist_play_outlined,
|
||||||
@@ -134,9 +130,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (song != null) {
|
|
||||||
navigatorKey.currentState?.pushNamed('/playlist');
|
navigatorKey.currentState?.pushNamed('/playlist');
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
@@ -147,6 +141,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user