10 Commits
Author SHA1 Message Date
lxh2875931338 40d4af322b 开始进入修复阶段 2026-09-02 21:42:42 +08:00
lxh2875931338 1aba004e43 定住这个commit,切换至test分支进行部分破坏性变更:涉及到app内多实例异常问题修复 2026-09-02 21:17:59 +08:00
lxh2875931338 cdf77b308d 开始给原先适配期的状态擦屁股:现在是修复播放页与播放列表页的显示 2026-09-02 20:44:46 +08:00
lxh2875931338 200067a3b2 回归GPL主线的第一版本,后续LGPL仍会继续测试,但是将不会在主分支加入测试 2026-09-02 20:23:06 +08:00
lxh2875931338 974dee2995 revert 99917bac87
revert Core Freeze:核心链路功能已经完成设计,后续进入beta功能开发模式,接下来会在UI部分微调后,进入首个Release版本(1.1.0-Release)
2026-09-02 10:15:54 +08:00
lxh2875931338 b86a5d2b46 准备进入新的测试阶段,先做一个snapshot(此版本下不可用) 2026-08-31 20:51:09 +08:00
lxh2875931338 eb46c53f9a 不考虑上架问题,确认此次更新 2026-08-31 00:05:13 +08:00
lxh2875931338 53f60d393f Merge remote-tracking branch 'origin/test' 2026-08-30 23:57:44 +08:00
lxh2875931338 08ea8a4508 test通道确认功能完整
libmpv.so(LGPL版本)
SHA256 = E56776A1BA03892C9A037A534A7114BA54F6EE202083A706CF878924BAA9D7AA
2026-08-30 23:57:24 +08:00
lxh2875931338 21a1ad1d05 确定这次重构的版本号 2026-08-30 21:55:51 +08:00
24 changed files with 408 additions and 263 deletions
+4
View File
@@ -7,6 +7,10 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
exclude:
- build/**
- android/**
include: package:flutter_lints/flutter.yaml
linter:
+11 -6
View File
@@ -20,25 +20,31 @@ android {
versionCode = flutter.versionCode
versionName = flutter.versionName
// 仅打包 arm64-v8a(与你的 jniLibs 目录匹配)
// 仅打包 arm64-v8a
ndk {
abiFilters.addAll(listOf("arm64-v8a"))
}
}
// 显式指定 jniLibs 源目录(确保 .so 文件被包含)
// 显式指定 jniLibs 源目录
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
}
}
// ⭐ 关键:打包配置,确保 .so 不被压缩,且处理重复
tasks.withType<Copy> {
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
from("src/main/jniLibs")
into("$buildDir/intermediates/merged_jni_libs/debug/out")
}
}
// 打包配置
packagingOptions {
jniLibs {
useLegacyPackaging = true // 兼容 Android 7.0+
useLegacyPackaging = true
}
pickFirsts += "**/*.so" // 如有重复则选第一个
pickFirsts += "**/*.so"
}
buildTypes {
@@ -47,7 +53,6 @@ android {
release {
signingConfig = signingConfigs.getByName("debug")
// ⭐ 建议开启混淆时保留某些 native 方法(media_kit 会自动处理)
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
+45
View File
@@ -0,0 +1,45 @@
# ============================================================
# media_kit 核心
# ============================================================
-keep class com.media_kit.** { *; }
-keep class com.alexmercerind.** { *; }
-keep class org.mozilla.** { *; }
# 保留所有 native 方法
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留所有 JNI 相关
-keep class * implements java.lang.reflect.Method { *; }
-keep class * extends java.lang.reflect.Method { *; }
# ============================================================
# audio_service 插件
# ============================================================
-keep class com.ryanheise.audioservice.** { *; }
# ============================================================
# Flutter 核心
# ============================================================
-keep class io.flutter.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.embedding.** { *; }
# ============================================================
# 忽略 Google Play Core 缺失类(如果不想添加依赖)
# ============================================================
-dontwarn com.google.android.play.core.**
-keep class com.google.android.play.core.** { *; }
# ============================================================
# 保留 Serializable 支持
# ============================================================
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
+1
View File
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application
android:name="${applicationName}"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,17 +4,66 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.core.content.FileProvider
import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import android.content.Context
import android.media.AudioManager
class MainActivity : AudioServiceActivity() {
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.lxh.qingting_player/file_provider"
override fun onCreate(savedInstanceState: Bundle?) {
// ⭐ 强制加载 libmediakitandroidhelper.so,触发 JNI_OnLoad,缓存 JavaVM
try {
System.loadLibrary("mediakitandroidhelper")
Log.d("MainActivity", "✅ libmediakitandroidhelper loaded")
} catch (e: UnsatisfiedLinkError) {
Log.e("MainActivity", "❌ libmediakitandroidhelper load failed", e)
}
// ⭐ 可选:预加载 libmpv.so(让系统 linker 也可见,增强兼容性)
try {
System.loadLibrary("mpv")
Log.d("MainActivity", "✅ libmpv loaded")
} catch (e: UnsatisfiedLinkError) {
Log.e("MainActivity", "❌ libmpv load failed", e)
}
super.onCreate(savedInstanceState)
requestAudioFocus()
setHighRefreshRate()
}
private fun requestAudioFocus() {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioManager.requestAudioFocus(
AudioManager.OnAudioFocusChangeListener { focusChange ->
Log.d("MainActivity", "音频焦点变化: $focusChange")
},
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
null,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
}
Log.d("MainActivity", "✅ 已请求音频焦点")
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// FileProvider MethodChannel for sharing artwork via content:// URI
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getContentUri") {
@@ -46,20 +95,6 @@ class MainActivity : AudioServiceActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 🔥 测试 libmpv.so 能否被系统加载
try {
System.loadLibrary("mpv")
Log.e("MPV_TEST", "🔥 libmpv LOAD SUCCESS")
} catch (e: Throwable) {
Log.e("MPV_TEST", "🔥 libmpv LOAD FAILED", e)
}
setHighRefreshRate()
}
override fun onResume() {
super.onResume()
setHighRefreshRate()
@@ -77,17 +112,4 @@ class MainActivity : AudioServiceActivity() {
Log.e("QTPlayer", "设置高刷失败: ${e.message}")
}
}
private fun clearRefreshRate() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val params = window.attributes
params.preferredRefreshRate = 0f
window.attributes = params
Log.d("QTPlayer", "恢复默认刷新率")
}
} catch (e: Exception) {
Log.e("QTPlayer", "恢复默认刷新率失败: ${e.message}")
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
-32
View File
@@ -1,32 +0,0 @@
// lib/audio/player_controller.dart
import '../services/playback_service.dart'; // ⭐ 修正路径
class PlayerController {
static final PlayerController _instance = PlayerController._internal();
factory PlayerController() => _instance;
PlayerController._internal();
final PlaybackService _playback = PlaybackService();
// ---- 状态流(只读) ----
Stream<bool> get playingStream => _playback.player.stream.playing;
Stream<Duration> get positionStream => _playback.player.stream.position;
Stream<Duration> get durationStream => _playback.player.stream.duration;
Stream<void> get completedStream => _playback.player.stream.completed;
// ---- 当前状态快照 ----
bool get isPlaying => _playback.player.state.playing;
Duration get position => _playback.player.state.position;
Duration get duration => _playback.player.state.duration;
// ---- 控制命令 ----
Future<void> play() => _playback.resume();
Future<void> pause() => _playback.pause();
Future<void> stop() => _playback.stop();
Future<void> seek(Duration position) => _playback.seek(position);
// ---- 加载歌曲 ----
void load(String url, {Map<String, String>? headers}) {
_playback.play(url, headers: headers);
}
}
+69 -57
View File
@@ -5,6 +5,7 @@ import 'package:media_kit/media_kit.dart';
import 'package:audio_service/audio_service.dart' as audio_service;
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/services.dart';
import 'services/audio_service.dart';
import 'services/playback_service.dart';
import 'services/app_lifecycle_service.dart';
@@ -15,8 +16,8 @@ import 'pages/home_page.dart';
import 'pages/player_page.dart';
import 'pages/playlist_page.dart';
import 'widgets/global_mini_player.dart';
import 'utils/navigation.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final stopwatch = Stopwatch();
bool _appInitialized = false;
@@ -53,6 +54,8 @@ void main() async {
try {
_audioHandler = AudioPlayerHandler();
debugPrint('⏱️ T0.62 AudioPlayerHandler created');
// ⭐ 将 Handler 注入到 AudioService
AudioService().setHandler(_audioHandler!);
} catch (e, st) {
debugPrint('❌ AudioPlayerHandler failed: $e');
debugPrint('$st');
@@ -363,65 +366,74 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
debugPrint(
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
return MaterialApp(
title: '清听',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF7C9A9E),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF7C9A9E),
secondary: Color(0xFFB8D4D0),
surface: Color(0xFF1A1F1E),
onSurface: Colors.white,
),
useMaterial3: true,
// ⭐ 使用 AnnotatedRegion 包裹 MaterialApp,统一控制系统栏样式
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: const Color(0xFF1A1F1E),
systemNavigationBarIconBrightness: Brightness.light,
),
navigatorKey: navigatorKey,
navigatorObservers: [MiniPlayerNavigatorObserver()],
onGenerateRoute: _onGenerateRoute,
builder: (context, child) {
return Stack(
fit: StackFit.expand,
children: [
if (child != null) child,
AnimatedBuilder(
animation: RouteManager(),
builder: (context, _) {
final routeManager = RouteManager();
final shouldShow = routeManager.currentRoute != '/player' &&
routeManager.currentRoute != '/playlist';
child: MaterialApp(
title: '清听',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF7C9A9E),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF7C9A9E),
secondary: Color(0xFFB8D4D0),
surface: Color(0xFF1A1F1E),
onSurface: Colors.white,
),
useMaterial3: true,
),
navigatorKey: navigatorKey,
navigatorObservers: [MiniPlayerNavigatorObserver()],
onGenerateRoute: _onGenerateRoute,
builder: (context, child) {
return Stack(
fit: StackFit.expand,
children: [
if (child != null) child,
AnimatedBuilder(
animation: RouteManager(),
builder: (context, _) {
final routeManager = RouteManager();
final shouldShow = routeManager.currentRoute != '/player' &&
routeManager.currentRoute != '/playlist';
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,
child: GlobalMiniPlayer(),
)
: const SizedBox.shrink(
key: ValueKey('mini_player_hidden'),
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,
child: GlobalMiniPlayer(),
)
: const SizedBox.shrink(
key: ValueKey('mini_player_hidden'),
),
);
},
),
],
);
},
),
);
}
}
+138 -93
View File
@@ -4,49 +4,62 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:audio_service/audio_service.dart' as audio_service;
import '../audio/player_controller.dart';
import '../audio/playback_state_manager.dart';
import '../services/audio_service.dart';
import '../utils/file_provider_utils.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'dart:convert';
import 'playback_service.dart';
import 'audio_service.dart';
import '../utils/file_provider_utils.dart';
/// 音频服务处理器:连接 media_kit 播放引擎与 Android 系统媒体会话
/// 职责:发布播放状态到系统通知栏,接收系统媒体按钮事件
/// 它不拥有 Player 实例,所有播放控制均委托给 PlaybackService
class AudioPlayerHandler extends audio_service.BaseAudioHandler {
final PlayerController _player = PlayerController();
final PlaybackStateManager _state = PlaybackStateManager();
final PlaybackService _playback = PlaybackService();
// 当前媒体信息
String? _currentId;
String? _currentTitle;
String? _currentArtist;
Duration _currentPosition = Duration.zero;
DateTime _lastPublishTime = DateTime.now();
static const Duration _publishInterval = Duration(milliseconds: 500);
// ⭐ 缓存 artwork 文件路径,避免重复写入
String? _currentArtworkPath;
bool _isPublishing = false;
AudioPlayerHandler() {
_player.playingStream.listen((playing) {
_state.updatePlaying(playing);
_publishState();
_bindToPlayer();
}
// ═══════════════════════════════════════════════════════════════
// 绑定播放器状态流
// ═══════════════════════════════════════════════════════════════
void _bindToPlayer() {
final player = _playback.player;
player.stream.playing.listen((playing) {
_publishPlaybackState(
playing: playing,
position: _currentPosition,
duration: player.state.duration,
);
});
_player.positionStream.listen((position) {
player.stream.position.listen((position) {
_currentPosition = position;
_state.updatePosition(position);
final now = DateTime.now();
if (now.difference(_lastPublishTime) >= _publishInterval) {
_lastPublishTime = now;
// ⭐ 只保留 _publishStateOnly()
_publishStateOnly();
_publishPlaybackState(
playing: player.state.playing,
position: position,
duration: player.state.duration,
onlyPosition: true,
);
}
});
_player.durationStream.listen((duration) {
debugPrint('🎯 [durationStream] duration=$duration');
_state.updateDuration(duration);
player.stream.duration.listen((duration) {
if (_currentId != null && _currentTitle != null) {
_updateMediaItem(
id: _currentId!,
@@ -55,45 +68,93 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
duration: duration,
);
}
_publishState();
_publishPlaybackState(
playing: player.state.playing,
position: _currentPosition,
duration: duration,
);
});
}
// ---- 发布状态 ----
void _publishState() {
final state = _state.playbackState;
playbackState.add(audio_service.PlaybackState(
controls: state.controls,
processingState: state.processingState,
playing: state.playing,
androidCompactActionIndices: state.androidCompactActionIndices,
updatePosition: _currentPosition,
updateTime: DateTime.now(),
systemActions: const {
audio_service.MediaAction.seek,
},
));
// ═══════════════════════════════════════════════════════════════
// 外部同步接口(由 AudioService 调用)
// ═══════════════════════════════════════════════════════════════
void syncState(Song? song) {
if (song == null) {
_currentId = null;
_currentTitle = null;
_currentArtist = null;
return;
}
_updateMediaItem(
id: song.id,
title: song.title,
artist: song.artist,
artwork: song.artwork,
);
final player = _playback.player;
_publishPlaybackState(
playing: player.state.playing,
position: _currentPosition,
duration: player.state.duration,
);
}
void _publishStateOnly() {
final current = playbackState.value;
// ═══════════════════════════════════════════════════════════════
// 状态发布
// ═══════════════════════════════════════════════════════════════
void _publishPlaybackState({
required bool playing,
required Duration position,
required Duration duration,
bool onlyPosition = false,
}) {
if (_isPublishing) return;
_isPublishing = true;
playbackState.add(audio_service.PlaybackState(
controls: current.controls.isNotEmpty
? current.controls
: _state.playbackState.controls,
processingState: _state.playbackState.processingState,
playing: current.playing,
androidCompactActionIndices: current.androidCompactActionIndices,
updatePosition: _currentPosition,
updateTime: DateTime.now(),
systemActions: const {
audio_service.MediaAction.seek,
},
));
try {
final controls = _buildControls(playing);
final state = audio_service.PlaybackState(
controls: controls,
processingState: duration.inMilliseconds > 0
? audio_service.AudioProcessingState.ready
: audio_service.AudioProcessingState.idle,
playing: playing,
updatePosition: position,
updateTime: DateTime.now(),
systemActions: const {audio_service.MediaAction.seek},
);
playbackState.add(state);
} finally {
_isPublishing = false;
}
}
// ---- 更新媒体信息 ----
List<audio_service.MediaControl> _buildControls(bool playing) {
return [
const audio_service.MediaControl(
androidIcon: 'drawable/ic_previous',
label: '上一曲',
action: audio_service.MediaAction.skipToPrevious,
),
audio_service.MediaControl(
androidIcon: playing ? 'drawable/ic_pause' : 'drawable/ic_play',
label: playing ? '暂停' : '播放',
action: audio_service.MediaAction.playPause,
),
const audio_service.MediaControl(
androidIcon: 'drawable/ic_next',
label: '下一曲',
action: audio_service.MediaAction.skipToNext,
),
];
}
// ═══════════════════════════════════════════════════════════════
// 媒体信息更新
// ═══════════════════════════════════════════════════════════════
void _updateMediaItem({
required String id,
required String title,
@@ -101,21 +162,15 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
Duration? duration,
Uint8List? artwork,
}) {
debugPrint(
'📢 [handler] _updateMediaItem: artwork is ${artwork != null ? 'not null (${artwork.length} bytes)' : 'null'}');
_currentId = id;
_currentTitle = title;
_currentArtist = artist;
final position = _player.position;
debugPrint('📢 [handler] updateMediaItem: $title - $artist');
final position = _playback.player.state.position;
// ⭐ 异步处理 artwork(不阻塞主流程)
_handleArtwork(id, artwork).then((artUri) {
// 如果 artUri 变化,重新推送 MediaItem
final current = mediaItem.value;
if (current != null && current.artUri != artUri) {
debugPrint('📢 [handler] updating artUri: $artUri');
mediaItem.add(audio_service.MediaItem(
id: current.id,
title: current.title,
@@ -127,38 +182,28 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
}
});
// 先推送不带封面图的 MediaItem(让 UI 尽快显示)
mediaItem.add(audio_service.MediaItem(
id: id,
title: title,
artist: artist,
duration: duration ?? _player.duration,
duration: duration ?? _playback.player.state.duration,
extras: {'position': position.inMilliseconds},
));
}
/// 处理封面图:保存到本地并生成 content URI
Future<Uri?> _handleArtwork(String id, Uint8List? artwork) async {
if (artwork == null || artwork.isEmpty) {
_currentArtworkPath = null;
return null;
}
if (artwork == null || artwork.isEmpty) return null;
try {
final dir = await getApplicationDocumentsDirectory();
final artworkDir = Directory('${dir.path}/artworks');
if (!await artworkDir.exists()) {
await artworkDir.create(recursive: true);
}
// 使用 md5 生成安全的文件名
final bytes = utf8.encode(id);
final digest = md5.convert(bytes);
final fileName = '$digest.jpg';
final path = '${artworkDir.path}/$fileName';
final file = File(path);
// 检查文件是否已存在
if (await file.exists()) {
final existingBytes = await file.readAsBytes();
if (existingBytes.length == artwork.length &&
@@ -167,12 +212,8 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
return await FileProviderUtils.getContentUri(file);
}
}
// 写入新文件
await file.writeAsBytes(artwork);
_currentArtworkPath = path;
debugPrint('📢 [handler] artwork saved: $path');
return await FileProviderUtils.getContentUri(file);
} catch (e) {
debugPrint('⚠️ [handler] artwork handling failed: $e');
@@ -180,50 +221,54 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
}
}
// ---- 外部接口 ----
void updateNotification({
required String id,
required String title,
required String artist,
Uint8List? artwork,
}) {
_updateMediaItem(
id: id,
title: title,
artist: artist,
artwork: artwork,
_updateMediaItem(id: id, title: title, artist: artist, artwork: artwork);
final player = _playback.player;
_publishPlaybackState(
playing: player.state.playing,
position: _currentPosition,
duration: player.state.duration,
);
_publishState();
}
// ---- 控制命令 ----
// ═══════════════════════════════════════════════════════════════
// audio_service 控制命令(全部委托给 PlaybackService
// ═══════════════════════════════════════════════════════════════
@override
Future<void> play() async {
debugPrint('▶️ [handler] play() called');
_player.play();
_publishState();
await _playback.resume();
}
@override
Future<void> pause() async {
debugPrint('⏸️ [handler] pause() called');
_player.pause();
_publishState();
await _playback.pause();
}
@override
Future<void> stop() async {
debugPrint('⏹️ [handler] stop() called');
_player.stop();
_publishState();
await _playback.stop();
}
@override
Future<void> seek(Duration position) async {
debugPrint('⏩ [handler] seek() called: $position');
await _player.seek(position);
await _playback.seek(position);
_currentPosition = position;
_publishStateOnly();
final player = _playback.player;
_publishPlaybackState(
playing: player.state.playing,
position: position,
duration: player.state.duration,
onlyPosition: true,
);
}
@override
@@ -251,14 +296,14 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
await skipToPrevious();
break;
case audio_service.MediaButton.media:
if (_player.isPlaying) {
if (_playback.player.state.playing) {
await pause();
} else {
await play();
}
break;
default:
if (_player.isPlaying) {
if (_playback.player.state.playing) {
await pause();
} else {
await play();
+20 -22
View File
@@ -13,6 +13,7 @@ import '../utils/artwork_helper.dart';
import '../repositories/playlist_repository.dart';
import '../models/playlist.dart';
import 'webdav_service.dart';
import 'audio_player_handler.dart';
enum PlayMode {
sequential,
@@ -62,6 +63,9 @@ class AudioService extends ChangeNotifier {
// ---- 当前播放的歌单 ID ----
String? _currentPlaylistId;
// ⭐ 插入位置:在现有成员变量之后,方法之前
AudioPlayerHandler? _handler; // ⭐ 添加这一行
// ---- 高频进度 ----
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
@@ -115,6 +119,11 @@ class AudioService extends ChangeNotifier {
_onSongChanged = callback;
}
void setHandler(AudioPlayerHandler handler) {
// ⭐ 添加这个方法
_handler = handler;
}
void togglePlayMode() {
switch (_playMode) {
case PlayMode.sequential:
@@ -264,6 +273,8 @@ class AudioService extends ChangeNotifier {
final song = _queue[_currentIndex];
_currentSong = song;
_handler?.syncState(song);
_startListening();
notifyListeners();
@@ -547,6 +558,8 @@ class AudioService extends ChangeNotifier {
}
void stopPlay() {
// 停止底层播放器
PlaybackService().stop(); // 新增
_currentSong = null;
_isPlaying = false;
positionNotifier.value = Duration.zero;
@@ -554,7 +567,6 @@ class AudioService extends ChangeNotifier {
bufferedNotifier.value = Duration.zero;
_stopListening();
notifyListeners();
// 停止时也保存一次
savePlaybackState();
}
@@ -779,6 +791,7 @@ class AudioService extends ChangeNotifier {
debugPrint('💾 [AudioService] playback state saved');
}
/// 恢复播放状态
/// 恢复播放状态
Future<bool> restorePlaybackState() async {
final state = await _db.getPlaybackState();
@@ -802,9 +815,7 @@ class AudioService extends ChangeNotifier {
_queue = queue;
_currentIndex = state['current_index'] as int;
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
_currentIndex = 0;
}
if (_currentIndex >= _queue.length) _currentIndex = 0;
final modeStr = state['play_mode'] as String? ?? 'sequential';
_playMode = modeStr == 'sequential'
@@ -814,20 +825,6 @@ class AudioService extends ChangeNotifier {
: PlayMode.shuffle;
_currentPlaylistId = state['current_playlist_id'] as String?;
// ⭐⭐⭐ 关键修复:重建 _shuffledIndices 和 _shuffledIndex ⭐⭐⭐
if (_playMode == PlayMode.shuffle) {
_shuffledIndices = List.generate(_queue.length, (i) => i);
_shuffledIndices.shuffle();
_shuffledIndex = _shuffledIndices.indexOf(_currentIndex);
if (_shuffledIndex == -1) {
_shuffledIndex = 0;
_currentIndex = _shuffledIndices[0];
}
} else {
_shuffledIndices = List.generate(_queue.length, (i) => i);
_shuffledIndex = _currentIndex;
}
// 保存待恢复的进度
final savedPos = state['position_ms'] as int? ?? 0;
_pendingSeekPosition = Duration(milliseconds: savedPos);
@@ -836,11 +833,12 @@ class AudioService extends ChangeNotifier {
_currentSong = song;
_onSongChanged?.call(song);
debugPrint('♻️ [AudioService] playback state restored: '
'queue=${_queue.length}, index=$_currentIndex, '
'song=${song.title}, mode=$_playMode');
_handler?.syncState(song); // ⭐ 添加这一行
// ⭐ 加载音频但不自动播放
debugPrint(
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
// ⭐ 关键:加载音频但不自动播放
await _loadRestoredPlayback();
return true;
+3
View File
@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
+5 -11
View File
@@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../main.dart';
import '../utils/navigation.dart'; // ⭐ 导入 navigatorKey
class GlobalMiniPlayer extends StatelessWidget {
const GlobalMiniPlayer({super.key});
@@ -13,7 +13,6 @@ class GlobalMiniPlayer extends StatelessWidget {
final isPlaying = context.select<AudioService, bool>((s) => s.isPlaying);
final bottomPadding = MediaQuery.of(context).padding.bottom;
// 如果没有歌曲,不显示
if (song == null) {
return const SizedBox.shrink();
}
@@ -21,7 +20,6 @@ class GlobalMiniPlayer extends StatelessWidget {
return Stack(
clipBehavior: Clip.none,
children: [
// 背景条
Positioned(
left: 0,
right: 0,
@@ -31,7 +29,6 @@ class GlobalMiniPlayer extends StatelessWidget {
color: const Color(0xFF1A1F1E),
),
),
// 主内容
Positioned(
left: 0,
right: 0,
@@ -44,21 +41,20 @@ class GlobalMiniPlayer extends StatelessWidget {
height: 60,
color: const Color(0xFF1A1F1E),
child: ClipRRect(
// ⭐ 修复涟漪圆角问题
borderRadius: BorderRadius.circular(12),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// ⭐ 使用 navigatorKey 直接跳转
navigatorKey.currentState?.pushNamed('/player');
},
highlightColor: Colors.white.withOpacity(0.05),
splashColor: Colors.white.withOpacity(0.1),
highlightColor: Colors.white.withValues(alpha: 0.05),
splashColor: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
child: Row(
children: [
const SizedBox(width: 12),
// 封面图
Container(
width: 44,
height: 44,
@@ -81,7 +77,6 @@ class GlobalMiniPlayer extends StatelessWidget {
: null,
),
const SizedBox(width: 12),
// 标题 + 艺术家
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -111,7 +106,6 @@ class GlobalMiniPlayer extends StatelessWidget {
],
),
),
// 播放/暂停按钮
IconButton(
icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
@@ -122,7 +116,6 @@ class GlobalMiniPlayer extends StatelessWidget {
context.read<AudioService>().togglePlay();
},
),
// 播放列表按钮
IconButton(
icon: const Icon(
Icons.playlist_play_outlined,
@@ -130,6 +123,7 @@ class GlobalMiniPlayer extends StatelessWidget {
size: 24,
),
onPressed: () {
// ⭐ 使用 navigatorKey 直接跳转
navigatorKey.currentState?.pushNamed('/playlist');
},
),
+58 -10
View File
@@ -284,10 +284,10 @@ packages:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
@@ -332,10 +332,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.19"
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
@@ -352,14 +352,62 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.6"
media_kit_libs_android_audio:
dependency: transitive
description:
name: media_kit_libs_android_audio
sha256: "8f8f9759e537e12d66f08bc4d5279eb1bb21a0ccc519ff3442c68a9f3b6dd68b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.8"
media_kit_libs_audio:
dependency: "direct main"
description:
name: media_kit_libs_audio
sha256: "81bf506c234e81e3ec536ba72f8f700a928543c14c345220210cae0411636316"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.7"
media_kit_libs_ios_audio:
dependency: transitive
description:
name: media_kit_libs_ios_audio
sha256: "78ccf04e27d6b4ba00a355578ccb39b772f00d48269a6ac3db076edf2d51934f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
media_kit_libs_linux:
dependency: transitive
description:
name: media_kit_libs_linux
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
media_kit_libs_macos_audio:
dependency: transitive
description:
name: media_kit_libs_macos_audio
sha256: "3be21844df98f286de32808592835073cdef2c1a10078bac135da790badca950"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
media_kit_libs_windows_audio:
dependency: transitive
description:
name: media_kit_libs_windows_audio
sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.9"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.0"
version: "1.19.0"
mime:
dependency: transitive
description:
@@ -721,10 +769,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
version: "0.7.12"
typed_data:
dependency: transitive
description:
@@ -761,10 +809,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
version: "2.4.2"
vm_service:
dependency: transitive
description:
+2 -2
View File
@@ -30,8 +30,8 @@ environment:
dependencies:
flutter:
sdk: flutter
media_kit: ^1.2.6
#media_kit_libs_audio: ^1.0.7
media_kit: 1.2.6
media_kit_libs_audio: ^1.0.7
shared_preferences: 2.2.2
provider: ^6.1.2
dio: ^5.4.0 # HTTP 客户端