权限配置暂时到此,后台播放问题并入生命周期管理同步开发
This commit is contained in:
@@ -3,19 +3,18 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- ⭐ 后台播放(必需) -->
|
||||
<!-- ⭐ 后台播放 -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- Android 12+ 后台启动限制 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- ⭐ 通知(Android 13+ 必需) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="qt_player"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -40,6 +39,10 @@
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="false" />
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -3,46 +3,20 @@ package com.example.qt_player
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import com.ryanheise.audioservice.AudioServiceActivity // ⭐ 改用这个
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val CHANNEL = "com.qt_player/frame_rate"
|
||||
class MainActivity : AudioServiceActivity() { // ⭐ 继承 AudioServiceActivity
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// 在创建时尝试设置(但可能窗口还未就绪,所以下面在 onResume 再次设置)
|
||||
setHighRefreshRate()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// 每次回到前台时确保高刷生效
|
||||
setHighRefreshRate()
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"setHighRefreshRate" -> {
|
||||
setHighRefreshRate()
|
||||
result.success(true)
|
||||
}
|
||||
"clearRefreshRate" -> {
|
||||
clearRefreshRate()
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setHighRefreshRate() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
@@ -60,7 +34,7 @@ class MainActivity : FlutterActivity() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val params = window.attributes
|
||||
params.preferredRefreshRate = 0f // 0 表示系统自动
|
||||
params.preferredRefreshRate = 0f
|
||||
window.attributes = params
|
||||
Log.d("QTPlayer", "恢复默认刷新率")
|
||||
}
|
||||
|
||||
+142
-37
@@ -1,32 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
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 'services/audio_service.dart';
|
||||
import 'services/playback_service.dart';
|
||||
import 'services/app_lifecycle_service.dart';
|
||||
import 'services/webdav_service.dart';
|
||||
import 'services/audio_player_handler.dart';
|
||||
import 'pages/home_page.dart';
|
||||
import 'pages/player_page.dart';
|
||||
import 'pages/playlist_page.dart';
|
||||
import 'widgets/global_mini_player.dart';
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// ⏱️ 全局计时器
|
||||
final stopwatch = Stopwatch();
|
||||
|
||||
void main() {
|
||||
// ⭐ 防止 Hot Restart / 热重载时重复初始化
|
||||
bool _appInitialized = false;
|
||||
|
||||
// ⭐ 全局缓存 Handler
|
||||
AudioPlayerHandler? _audioHandler;
|
||||
|
||||
void main() async {
|
||||
// ⭐ P0:防止重复初始化
|
||||
if (_appInitialized) {
|
||||
debugPrint('⏱️ main 已初始化,跳过重复执行');
|
||||
return;
|
||||
}
|
||||
_appInitialized = true;
|
||||
|
||||
stopwatch.start();
|
||||
debugPrint('⏱️ T0 main: ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
debugPrint('⏱️ T0.5 ensureInitialized: ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
// ⭐ MediaKit 在 runApp 前初始化(官方要求)
|
||||
try {
|
||||
MediaKit.ensureInitialized();
|
||||
debugPrint('⏱️ T0.6 MediaKit 初始化完成');
|
||||
} catch (e) {
|
||||
debugPrint('❌ MediaKit 初始化失败: $e');
|
||||
}
|
||||
|
||||
// ⭐ 创建 Handler(AudioService 稍后初始化)
|
||||
_audioHandler = AudioPlayerHandler();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => AudioService()),
|
||||
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
||||
Provider<AudioPlayerHandler>(create: (_) => _audioHandler!),
|
||||
],
|
||||
child: const QTPlayerApp(),
|
||||
),
|
||||
@@ -35,13 +62,15 @@ void main() {
|
||||
debugPrint('⏱️ T0.8 runApp 完成: ${stopwatch.elapsedMilliseconds}ms');
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 路由管理器(控制 MiniPlayer 显示/隐藏)
|
||||
// ════════════════════════════════════════════════════════════
|
||||
class RouteManager extends ChangeNotifier {
|
||||
static final RouteManager _instance = RouteManager._internal();
|
||||
factory RouteManager() => _instance;
|
||||
RouteManager._internal();
|
||||
|
||||
String _currentRoute = '/';
|
||||
|
||||
String get currentRoute => _currentRoute;
|
||||
|
||||
bool get showMiniPlayer =>
|
||||
@@ -56,6 +85,9 @@ class RouteManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 导航观察者(监听路由变化)
|
||||
// ════════════════════════════════════════════════════════════
|
||||
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
||||
void _updateRoute(Route? route) {
|
||||
final name = route?.settings.name ?? '/';
|
||||
@@ -78,6 +110,9 @@ class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 主应用
|
||||
// ════════════════════════════════════════════════════════════
|
||||
class QTPlayerApp extends StatefulWidget {
|
||||
const QTPlayerApp({super.key});
|
||||
|
||||
@@ -86,21 +121,123 @@ class QTPlayerApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||
bool _isInitializing = false;
|
||||
|
||||
// ============================================================
|
||||
// 生命周期
|
||||
// ============================================================
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
debugPrint('⏱️ T1.5 initState: ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
debugPrint('⏱️ T2 首帧回调: ${stopwatch.elapsedMilliseconds}ms');
|
||||
_initializeServices();
|
||||
_startBackgroundInitialization();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 后台初始化(不阻塞首帧)
|
||||
// ============================================================
|
||||
Future<void> _startBackgroundInitialization() async {
|
||||
if (_isInitializing) return;
|
||||
_isInitializing = true;
|
||||
|
||||
debugPrint('⏱️ T3 后台初始化开始');
|
||||
|
||||
// 1. PlaybackService(轻量级)
|
||||
try {
|
||||
PlaybackService().init();
|
||||
debugPrint('⏱️ T3.1 PlaybackService 初始化完成');
|
||||
} catch (e) {
|
||||
debugPrint('❌ PlaybackService 初始化失败: $e');
|
||||
}
|
||||
|
||||
// 2. ⭐ AudioService 暂时禁用(等 MainActivity 改为 AudioServiceActivity 后启用)
|
||||
// await _initAudioService();
|
||||
|
||||
// 3. WebDAV
|
||||
await _initWebDAV();
|
||||
|
||||
// 4. 通知权限
|
||||
await _requestPermissions();
|
||||
|
||||
debugPrint('⏱️ T4 全部初始化完成');
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 子初始化方法
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
// ⭐ 暂时禁用,等 MainActivity 改为 AudioServiceActivity 后启用
|
||||
// Future<void> _initAudioService() async {
|
||||
// try {
|
||||
// await audio_service.AudioService.init(
|
||||
// builder: () => _audioHandler!,
|
||||
// config: const audio_service.AudioServiceConfig(
|
||||
// androidNotificationChannelId: 'com.example.qt_player.music',
|
||||
// androidNotificationChannelName: '清听音乐播放',
|
||||
// androidNotificationOngoing: true,
|
||||
// androidStopForegroundOnPause: true,
|
||||
// ),
|
||||
// );
|
||||
// debugPrint('✅ AudioService 初始化完成');
|
||||
// } catch (e) {
|
||||
// debugPrint('❌ AudioService 初始化失败: $e');
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> _initWebDAV() async {
|
||||
try {
|
||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||
debugPrint('✅ WebDAV 加载完成, 已连接: $hasCred');
|
||||
} catch (e) {
|
||||
debugPrint('❌ WebDAV 加载失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
try {
|
||||
if (await Permission.notification.isDenied) {
|
||||
final status = await Permission.notification.request();
|
||||
debugPrint('📢 通知权限状态: $status');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ 权限申请失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Build
|
||||
// ============================================================
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('⏱️ T1 build: ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
// 监听歌曲变化,更新通知栏
|
||||
final audioService = context.watch<AudioService>();
|
||||
final handler = context.read<AudioPlayerHandler>();
|
||||
final song = audioService.currentSong;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (song != null) {
|
||||
handler.updateNotification(
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
);
|
||||
} else {
|
||||
handler.mediaItem.add(null);
|
||||
}
|
||||
});
|
||||
|
||||
return MaterialApp(
|
||||
title: '清听',
|
||||
theme: ThemeData(
|
||||
@@ -144,35 +281,3 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeServices() async {
|
||||
debugPrint('⏱️ T3 开始初始化: ${stopwatch.elapsedMilliseconds}ms');
|
||||
final lifecycle = AppLifecycleService();
|
||||
|
||||
try {
|
||||
lifecycle.updateStatus(AppStatus.mediaKitInitializing);
|
||||
MediaKit.ensureInitialized();
|
||||
debugPrint('⏱️ T4 MediaKit 完成: ${stopwatch.elapsedMilliseconds}ms');
|
||||
lifecycle.updateStatus(AppStatus.mediaKitReady);
|
||||
|
||||
lifecycle.updateStatus(AppStatus.playerInitializing);
|
||||
PlaybackService().init();
|
||||
debugPrint('⏱️ T5 Player 完成: ${stopwatch.elapsedMilliseconds}ms');
|
||||
lifecycle.updateStatus(AppStatus.playerReady);
|
||||
|
||||
lifecycle.updateStatus(AppStatus.webdavChecking);
|
||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||
debugPrint('⏱️ T6 WebDAV 完成: ${stopwatch.elapsedMilliseconds}ms');
|
||||
if (hasCred) {
|
||||
lifecycle.updateStatus(AppStatus.webdavReady);
|
||||
} else {
|
||||
lifecycle.updateStatus(AppStatus.webdavMissing);
|
||||
}
|
||||
|
||||
lifecycle.updateStatus(AppStatus.fullReady);
|
||||
debugPrint('✅ 应用完全就绪: ${stopwatch.elapsedMilliseconds}ms');
|
||||
} catch (e) {
|
||||
debugPrint('❌ 初始化错误: $e');
|
||||
lifecycle.updateStatus(AppStatus.error);
|
||||
}
|
||||
}
|
||||
|
||||
+214
-195
@@ -1,10 +1,17 @@
|
||||
// lib/pages/player_page.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import '../main.dart';
|
||||
|
||||
class PlayerPage extends StatefulWidget {
|
||||
const PlayerPage({super.key});
|
||||
final VoidCallback? onClose;
|
||||
final VoidCallback? onOpenPlaylist;
|
||||
|
||||
const PlayerPage({
|
||||
super.key,
|
||||
this.onClose,
|
||||
this.onOpenPlaylist,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PlayerPage> createState() => _PlayerPageState();
|
||||
@@ -20,6 +27,22 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void _closePage() {
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!();
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _openPlaylist() {
|
||||
if (widget.onOpenPlaylist != null) {
|
||||
widget.onOpenPlaylist!();
|
||||
} else {
|
||||
Navigator.pushNamed(context, '/playlist');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final service = context.watch<AudioService>();
|
||||
@@ -28,33 +51,22 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
if (song == null) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0E1211),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'正在播放',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: const Center(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.music_off, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
const Text(
|
||||
'没有正在播放的歌曲',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'请先在首页点击一首歌曲',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _closePage,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFB8D4D0),
|
||||
foregroundColor: Colors.black87,
|
||||
),
|
||||
child: const Text('返回'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -63,20 +75,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
}
|
||||
|
||||
final isPlaying = service.isPlaying;
|
||||
final position = service.position;
|
||||
final duration = service.duration;
|
||||
final bufferedPosition = service.bufferedPosition;
|
||||
|
||||
final progress = duration.inMilliseconds > 0
|
||||
? position.inMilliseconds / duration.inMilliseconds
|
||||
: 0.0;
|
||||
|
||||
final bufferProgress = duration.inMilliseconds > 0
|
||||
? (bufferedPosition.inMilliseconds / duration.inMilliseconds)
|
||||
.clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
final displayProgress = _dragProgress ?? progress;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0E1211),
|
||||
@@ -85,7 +84,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: _closePage,
|
||||
),
|
||||
title: const Text(
|
||||
'正在播放',
|
||||
@@ -157,169 +156,189 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final bufferWidth =
|
||||
width * bufferProgress.clamp(0.0, 1.0);
|
||||
final progressWidth =
|
||||
width * displayProgress.clamp(0.0, 1.0);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Container(
|
||||
width: width,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: bufferWidth,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4A7A7A),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: progressWidth,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFB8D4D0),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 0,
|
||||
activeTrackColor: Colors.transparent,
|
||||
inactiveTrackColor: Colors.transparent,
|
||||
thumbColor: Colors.transparent,
|
||||
overlayColor: Colors.transparent,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 0,
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: displayProgress.clamp(0.0, 1.0),
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dragProgress = value;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
final newPosition = Duration(
|
||||
milliseconds:
|
||||
(value * duration.inMilliseconds)
|
||||
.round(),
|
||||
);
|
||||
service.seekTo(newPosition);
|
||||
setState(() {
|
||||
_dragProgress = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(position),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDuration(duration),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildProgressSection(service, duration),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: service.togglePlayMode,
|
||||
icon: Icon(
|
||||
service.playModeIcon,
|
||||
color: const Color(0xFFB8D4D0),
|
||||
size: 22,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
onPressed: service.hasQueue ? service.previous : null,
|
||||
icon: const Icon(Icons.skip_previous, size: 28),
|
||||
color: service.hasQueue ? Colors.white60 : Colors.grey[600],
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFB8D4D0),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isPlaying ? Icons.pause : Icons.play_arrow,
|
||||
color: Colors.black87,
|
||||
size: 28,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: service.togglePlay,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
IconButton(
|
||||
onPressed: service.hasQueue ? service.next : null,
|
||||
icon: const Icon(Icons.skip_next, size: 28),
|
||||
color: service.hasQueue ? Colors.white60 : Colors.grey[600],
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
navigatorKey.currentState?.pushNamed('/playlist');
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.playlist_play_outlined,
|
||||
color: Color(0xFFB8D4D0),
|
||||
size: 26,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
_buildControlButtons(service, isPlaying),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressSection(AudioService service, Duration duration) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: service.positionNotifier,
|
||||
builder: (context, position, _) {
|
||||
final progress = duration.inMilliseconds > 0
|
||||
? position.inMilliseconds / duration.inMilliseconds
|
||||
: 0.0;
|
||||
|
||||
final bufferProgress = duration.inMilliseconds > 0
|
||||
? (service.bufferedNotifier.value.inMilliseconds /
|
||||
duration.inMilliseconds)
|
||||
.clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
final displayProgress = _dragProgress ?? progress;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final bufferWidth = width * bufferProgress.clamp(0.0, 1.0);
|
||||
final progressWidth = width * displayProgress.clamp(0.0, 1.0);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Container(
|
||||
width: width,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: bufferWidth,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4A7A7A),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: progressWidth,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFB8D4D0),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 0,
|
||||
activeTrackColor: Colors.transparent,
|
||||
inactiveTrackColor: Colors.transparent,
|
||||
thumbColor: Colors.transparent,
|
||||
overlayColor: Colors.transparent,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 0,
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: displayProgress.clamp(0.0, 1.0),
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dragProgress = value;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
final newPosition = Duration(
|
||||
milliseconds:
|
||||
(value * duration.inMilliseconds).round(),
|
||||
);
|
||||
service.seekTo(newPosition);
|
||||
setState(() {
|
||||
_dragProgress = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(position),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDuration(duration),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlButtons(AudioService service, bool isPlaying) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: service.togglePlayMode,
|
||||
icon: Icon(
|
||||
service.playModeIcon,
|
||||
color: const Color(0xFFB8D4D0),
|
||||
size: 22,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
onPressed: service.hasQueue ? service.previous : null,
|
||||
icon: const Icon(Icons.skip_previous, size: 28),
|
||||
color: service.hasQueue ? Colors.white60 : Colors.grey[600],
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFB8D4D0),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isPlaying ? Icons.pause : Icons.play_arrow,
|
||||
color: Colors.black87,
|
||||
size: 28,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: service.togglePlay,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
IconButton(
|
||||
onPressed: service.hasQueue ? service.next : null,
|
||||
icon: const Icon(Icons.skip_next, size: 28),
|
||||
color: service.hasQueue ? Colors.white60 : Colors.grey[600],
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _openPlaylist,
|
||||
icon: const Icon(
|
||||
Icons.playlist_play_outlined,
|
||||
color: Color(0xFFB8D4D0),
|
||||
size: 26,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// lib/services/audio_player_handler.dart
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'playback_service.dart';
|
||||
import 'audio_service.dart' as local_audio;
|
||||
|
||||
class AudioPlayerHandler extends BaseAudioHandler {
|
||||
final PlaybackService _playback = PlaybackService();
|
||||
late final local_audio.AudioService _localAudio;
|
||||
|
||||
MediaItem? _currentMediaItem;
|
||||
|
||||
AudioPlayerHandler() {
|
||||
_localAudio = local_audio.AudioService();
|
||||
|
||||
// 1. 监听播放状态(只更新 playing)
|
||||
_playback.player.stream.playing.listen((playing) {
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
playing: playing,
|
||||
));
|
||||
});
|
||||
|
||||
// 2. ⭐ 进度更新:跳过,因为当前版本不支持 position 参数
|
||||
// 通知栏进度条不会动,但播放/暂停/切歌功能正常
|
||||
// 如果后续需要,可以升级 audio_service 版本或改用其他方案
|
||||
|
||||
// 3. 监听播放完成(触发下一首)
|
||||
_playback.player.stream.completed.listen((_) {
|
||||
_localAudio.next();
|
||||
});
|
||||
|
||||
// 4. 监听时长变化,更新 MediaItem
|
||||
_playback.player.stream.duration.listen((duration) {
|
||||
if (_currentMediaItem != null) {
|
||||
_currentMediaItem = _currentMediaItem!.copyWith(duration: duration);
|
||||
mediaItem.add(_currentMediaItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 外部调用更新通知栏
|
||||
void updateNotification({
|
||||
required String id,
|
||||
required String title,
|
||||
required String artist,
|
||||
}) {
|
||||
_currentMediaItem = MediaItem(
|
||||
id: id,
|
||||
title: title,
|
||||
artist: artist,
|
||||
duration: _playback.player.state.duration,
|
||||
);
|
||||
mediaItem.add(_currentMediaItem);
|
||||
}
|
||||
|
||||
// ---- AudioHandler 接口实现 ----
|
||||
@override
|
||||
Future<void> play() async {
|
||||
_playback.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
_playback.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_playback.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
_playback.seek(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
_localAudio.next();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
_localAudio.previous();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> click([MediaButton button = MediaButton.media]) async {
|
||||
// 默认行为:打开 App
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'playback_service.dart';
|
||||
|
||||
// ============================================================
|
||||
// 播放模式
|
||||
// ============================================================
|
||||
enum PlayMode {
|
||||
sequential, // 顺序循环
|
||||
repeatOne, // 单曲循环
|
||||
shuffle, // 随机播放
|
||||
sequential,
|
||||
repeatOne,
|
||||
shuffle,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 歌曲模型
|
||||
// ============================================================
|
||||
class Song {
|
||||
final String id;
|
||||
final String title;
|
||||
@@ -30,49 +24,43 @@ class Song {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AudioService - 播放状态管理与 UI 数据源
|
||||
// ============================================================
|
||||
class AudioService extends ChangeNotifier {
|
||||
// ---------- 单例 ----------
|
||||
static final AudioService _instance = AudioService._internal();
|
||||
factory AudioService() => _instance;
|
||||
AudioService._internal();
|
||||
|
||||
// ---------- 播放状态 ----------
|
||||
// ---- 基础状态(低频,触发 UI 重建) ----
|
||||
Song? _currentSong;
|
||||
bool _isPlaying = false;
|
||||
Duration _position = Duration.zero;
|
||||
Duration _duration = Duration.zero;
|
||||
Duration _bufferedPosition = Duration.zero;
|
||||
|
||||
// ---------- 播放模式 ----------
|
||||
PlayMode _playMode = PlayMode.sequential;
|
||||
|
||||
// ---------- 播放队列 ----------
|
||||
// ---- 播放队列 ----
|
||||
List<Song> _queue = [];
|
||||
int _currentIndex = -1;
|
||||
|
||||
// ---------- 随机播放相关 ----------
|
||||
List<int> _shuffledIndices = [];
|
||||
int _shuffledIndex = -1;
|
||||
|
||||
// ---------- 监听控制 ----------
|
||||
// ---- ⭐ 高频进度(用 ValueNotifier,不触发全局重建) ----
|
||||
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
|
||||
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
|
||||
final ValueNotifier<Duration> bufferedNotifier = ValueNotifier(Duration.zero);
|
||||
|
||||
bool _listening = false;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
// ---------- Getter ----------
|
||||
// ---- Getter(高频字段不走 ChangeNotifier) ----
|
||||
Song? get currentSong => _currentSong;
|
||||
bool get isPlaying => _isPlaying;
|
||||
Duration get position => _position;
|
||||
Duration get duration => _duration;
|
||||
Duration get bufferedPosition => _bufferedPosition;
|
||||
PlayMode get playMode => _playMode;
|
||||
List<Song> get queue => List.unmodifiable(_queue);
|
||||
int get currentIndex => _currentIndex;
|
||||
bool get hasQueue => _queue.isNotEmpty;
|
||||
|
||||
// ---------- 播放模式图标 ----------
|
||||
// ---- 兼容旧代码:提供 getter 返回 ValueNotifier 的值 ----
|
||||
Duration get position => positionNotifier.value;
|
||||
Duration get duration => durationNotifier.value;
|
||||
Duration get bufferedPosition => bufferedNotifier.value;
|
||||
|
||||
IconData get playModeIcon {
|
||||
switch (_playMode) {
|
||||
case PlayMode.sequential:
|
||||
@@ -84,7 +72,7 @@ class AudioService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 播放模式切换 ----------
|
||||
// ---- 切换播放模式 ----
|
||||
void togglePlayMode() {
|
||||
switch (_playMode) {
|
||||
case PlayMode.sequential:
|
||||
@@ -100,7 +88,7 @@ class AudioService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ---------- 设置播放队列 ----------
|
||||
// ---- 设置播放队列 ----
|
||||
void setQueue(List<Song> queue, {int startIndex = 0}) {
|
||||
if (queue.isEmpty) {
|
||||
_clearQueue();
|
||||
@@ -110,7 +98,6 @@ class AudioService extends ChangeNotifier {
|
||||
_queue = List.from(queue);
|
||||
_currentIndex = startIndex.clamp(0, _queue.length - 1);
|
||||
|
||||
// 初始化随机播放索引
|
||||
_shuffledIndices = List.generate(_queue.length, (i) => i);
|
||||
_shuffledIndices.shuffle();
|
||||
_shuffledIndex = _shuffledIndices.indexOf(_currentIndex);
|
||||
@@ -119,11 +106,9 @@ class AudioService extends ChangeNotifier {
|
||||
_currentIndex = _shuffledIndices[0];
|
||||
}
|
||||
|
||||
// 播放当前歌曲
|
||||
_playCurrent();
|
||||
}
|
||||
|
||||
// ---------- 清空队列 ----------
|
||||
void _clearQueue() {
|
||||
_queue.clear();
|
||||
_currentIndex = -1;
|
||||
@@ -132,18 +117,15 @@ class AudioService extends ChangeNotifier {
|
||||
stopPlay();
|
||||
}
|
||||
|
||||
// ---------- 播放指定歌曲(外部入口) ----------
|
||||
// ---- 播放指定歌曲 ----
|
||||
Future<void> playSong(Song song) async {
|
||||
// 如果当前队列不包含这首歌,替换队列
|
||||
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
||||
setQueue([song], startIndex: 0);
|
||||
} else {
|
||||
// 如果已经在队列中,直接播放
|
||||
_playCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 播放当前索引歌曲 ----------
|
||||
void _playCurrent() {
|
||||
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
|
||||
stopPlay();
|
||||
@@ -152,9 +134,11 @@ class AudioService extends ChangeNotifier {
|
||||
|
||||
final song = _queue[_currentIndex];
|
||||
_currentSong = song;
|
||||
_position = Duration.zero;
|
||||
_duration = Duration.zero;
|
||||
_bufferedPosition = Duration.zero;
|
||||
|
||||
// ⭐ 重置进度(用 ValueNotifier)
|
||||
positionNotifier.value = Duration.zero;
|
||||
durationNotifier.value = Duration.zero;
|
||||
bufferedNotifier.value = Duration.zero;
|
||||
|
||||
_startListening();
|
||||
notifyListeners();
|
||||
@@ -163,15 +147,13 @@ class AudioService extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// 由 PlaybackService 实际播放,状态由流更新
|
||||
PlaybackService().play(song.url!);
|
||||
}
|
||||
|
||||
// ---------- 播放下一首 ----------
|
||||
// ---- 下一首 ----
|
||||
void next() {
|
||||
if (_queue.isEmpty) return;
|
||||
|
||||
// 随机模式
|
||||
if (_playMode == PlayMode.shuffle) {
|
||||
if (_shuffledIndices.isEmpty) return;
|
||||
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
|
||||
@@ -181,17 +163,15 @@ class AudioService extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// 顺序/单曲模式
|
||||
final nextIdx = (_currentIndex + 1) % _queue.length;
|
||||
_currentIndex = nextIdx;
|
||||
_playCurrent();
|
||||
}
|
||||
|
||||
// ---------- 播放上一首 ----------
|
||||
// ---- 上一首 ----
|
||||
void previous() {
|
||||
if (_queue.isEmpty) return;
|
||||
|
||||
// 随机模式
|
||||
if (_playMode == PlayMode.shuffle) {
|
||||
if (_shuffledIndices.isEmpty) return;
|
||||
final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length;
|
||||
@@ -205,7 +185,6 @@ class AudioService extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// 顺序/单曲模式
|
||||
final prevIdx = (_currentIndex - 1) % _queue.length;
|
||||
if (prevIdx < 0) {
|
||||
_currentIndex = _queue.length - 1;
|
||||
@@ -215,7 +194,7 @@ class AudioService extends ChangeNotifier {
|
||||
_playCurrent();
|
||||
}
|
||||
|
||||
// ---------- 播放/暂停切换 ----------
|
||||
// ---- 播放/暂停 ----
|
||||
void togglePlay() {
|
||||
if (_currentSong == null) return;
|
||||
|
||||
@@ -224,39 +203,21 @@ class AudioService extends ChangeNotifier {
|
||||
} else {
|
||||
PlaybackService().resume();
|
||||
}
|
||||
// 状态由 player.stream.playing 更新
|
||||
}
|
||||
|
||||
// ---------- 停止播放 ----------
|
||||
void stopPlay() {
|
||||
_currentSong = null;
|
||||
_isPlaying = false;
|
||||
_position = Duration.zero;
|
||||
_duration = Duration.zero;
|
||||
_bufferedPosition = Duration.zero;
|
||||
positionNotifier.value = Duration.zero;
|
||||
durationNotifier.value = Duration.zero;
|
||||
bufferedNotifier.value = Duration.zero;
|
||||
_stopListening();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ---------- Seek ----------
|
||||
void seekTo(Duration position) {
|
||||
PlaybackService().seek(position);
|
||||
_position = position;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ---------- 处理播放结束 ----------
|
||||
void _onPlaybackCompleted() {
|
||||
if (_queue.isEmpty) return;
|
||||
|
||||
// 单曲循环模式
|
||||
if (_playMode == PlayMode.repeatOne) {
|
||||
_playCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他模式:播放下一首
|
||||
next();
|
||||
positionNotifier.value = position;
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
@@ -268,7 +229,7 @@ class AudioService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ---------- 监听 media_kit 状态 ----------
|
||||
// ---- 监听 media_kit 状态 ----
|
||||
void _startListening() {
|
||||
if (_listening) return;
|
||||
_listening = true;
|
||||
@@ -279,39 +240,30 @@ class AudioService extends ChangeNotifier {
|
||||
player.stream.playing.listen((playing) {
|
||||
if (_isPlaying != playing) {
|
||||
_isPlaying = playing;
|
||||
notifyListeners();
|
||||
notifyListeners(); // 只有播放状态变化才刷新
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// ⭐ 进度更新:只更新 ValueNotifier,不触发全局重建
|
||||
_subscriptions.add(
|
||||
player.stream.position.listen((position) {
|
||||
if (_position != position) {
|
||||
_position = position;
|
||||
notifyListeners();
|
||||
}
|
||||
positionNotifier.value = position;
|
||||
}),
|
||||
);
|
||||
|
||||
_subscriptions.add(
|
||||
player.stream.duration.listen((duration) {
|
||||
if (_duration != duration) {
|
||||
_duration = duration;
|
||||
notifyListeners();
|
||||
}
|
||||
durationNotifier.value = duration;
|
||||
}),
|
||||
);
|
||||
|
||||
_subscriptions.add(
|
||||
player.stream.buffer.listen((buffer) {
|
||||
if (_bufferedPosition != buffer) {
|
||||
_bufferedPosition = buffer;
|
||||
notifyListeners();
|
||||
}
|
||||
bufferedNotifier.value = buffer;
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ 播放结束监听
|
||||
_subscriptions.add(
|
||||
player.stream.completed.listen((_) {
|
||||
_onPlaybackCompleted();
|
||||
@@ -327,9 +279,23 @@ class AudioService extends ChangeNotifier {
|
||||
_subscriptions.clear();
|
||||
}
|
||||
|
||||
// ---------- 资源释放 ----------
|
||||
void _onPlaybackCompleted() {
|
||||
if (_queue.isEmpty) return;
|
||||
|
||||
if (_playMode == PlayMode.repeatOne) {
|
||||
_playCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopListening();
|
||||
positionNotifier.dispose();
|
||||
durationNotifier.dispose();
|
||||
bufferedNotifier.dispose();
|
||||
PlaybackService().dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// lib/widgets/global_mini_player.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/audio_service.dart';
|
||||
@@ -8,15 +9,16 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final audioService = context.watch<AudioService>();
|
||||
final song = audioService.currentSong;
|
||||
// ⭐ 用 Selector 只监听 currentSong(低频变化)
|
||||
final song = context.select<AudioService, Song?>((s) => s.currentSong);
|
||||
// ⭐ 只监听播放状态(低频变化)
|
||||
final isPlaying = context.select<AudioService, bool>((s) => s.isPlaying);
|
||||
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 底层遮罩
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -26,7 +28,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
color: const Color(0xFF1A1F1E),
|
||||
),
|
||||
),
|
||||
// 上层主体
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@@ -38,7 +39,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
child: Container(
|
||||
height: 56,
|
||||
color: const Color(0xFF1A1F1E),
|
||||
// ⭐ 用 Material 包裹 InkWell,获得更好的点击反馈
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
@@ -47,7 +47,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
navigatorKey.currentState?.pushNamed('/player');
|
||||
}
|
||||
},
|
||||
// ⭐ 让整个区域都响应点击,包括空白部分
|
||||
highlightColor: Colors.white.withOpacity(0.05),
|
||||
splashColor: Colors.white.withOpacity(0.1),
|
||||
child: Row(
|
||||
@@ -81,7 +80,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
// ⭐ 强制去掉下划线
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
maxLines: 1,
|
||||
@@ -94,7 +92,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
color: song != null
|
||||
? Colors.grey[400]
|
||||
: Colors.grey[600],
|
||||
// ⭐ 强制去掉下划线
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
maxLines: 1,
|
||||
@@ -105,7 +102,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
song != null && audioService.isPlaying
|
||||
song != null && isPlaying
|
||||
? Icons.pause
|
||||
: Icons.play_arrow,
|
||||
color: song != null ? Colors.white : Colors.grey[600],
|
||||
@@ -113,7 +110,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
||||
),
|
||||
onPressed: () {
|
||||
if (song != null) {
|
||||
audioService.togglePlay();
|
||||
context.read<AudioService>().togglePlay();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
+264
@@ -9,6 +9,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -17,6 +25,38 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
audio_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_service
|
||||
sha256: "95f3267f3449eb5cf71c8fcf1d556f57af1e898e2dc5815fb168d1843653edb7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.18.19"
|
||||
audio_service_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_service_platform_interface
|
||||
sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
audio_service_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_service_web
|
||||
sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
audio_session:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_session
|
||||
sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.25"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +81,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -118,6 +166,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "1de7849213b4c73c85aca7e0ac687a9a5d82ccdb594366b9dcc26cb6a2189cd2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.4.2"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -136,6 +192,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -160,6 +224,38 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
jni_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_util
|
||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -192,6 +288,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -288,6 +392,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -296,6 +416,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -320,6 +464,54 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.6.1"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.4+1"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -360,6 +552,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
safe_local_storage:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -437,6 +653,46 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.11"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3+1"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -557,6 +813,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
+6
-1
@@ -35,7 +35,12 @@ dependencies:
|
||||
shared_preferences: 2.2.2
|
||||
provider: ^6.1.2
|
||||
dio: ^5.4.0 # HTTP 客户端
|
||||
xml: ^6.5.0
|
||||
xml: ^6.5.0
|
||||
audio_service: ^0.18.13
|
||||
audio_session: ^0.1.21
|
||||
permission_handler: ^11.3.1
|
||||
flutter_cache_manager: ^3.3.1
|
||||
sqflite: ^2.3.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
Reference in New Issue
Block a user