权限配置暂时到此,后台播放问题并入生命周期管理同步开发
This commit is contained in:
@@ -3,19 +3,18 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<!-- ⭐ 后台播放(必需) -->
|
<!-- ⭐ 后台播放 -->
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<!-- Android 12+ 后台启动限制 -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
|
||||||
|
|
||||||
<!-- ⭐ 通知(Android 13+ 必需) -->
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="qt_player"
|
android:label="qt_player"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -40,6 +39,10 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
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
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|||||||
@@ -3,46 +3,20 @@ package com.example.qt_player
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import com.ryanheise.audioservice.AudioServiceActivity // ⭐ 改用这个
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : AudioServiceActivity() { // ⭐ 继承 AudioServiceActivity
|
||||||
|
|
||||||
private val CHANNEL = "com.qt_player/frame_rate"
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
// 在创建时尝试设置(但可能窗口还未就绪,所以下面在 onResume 再次设置)
|
|
||||||
setHighRefreshRate()
|
setHighRefreshRate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
// 每次回到前台时确保高刷生效
|
|
||||||
setHighRefreshRate()
|
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() {
|
private fun setHighRefreshRate() {
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
@@ -60,7 +34,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val params = window.attributes
|
val params = window.attributes
|
||||||
params.preferredRefreshRate = 0f // 0 表示系统自动
|
params.preferredRefreshRate = 0f
|
||||||
window.attributes = params
|
window.attributes = params
|
||||||
Log.d("QTPlayer", "恢复默认刷新率")
|
Log.d("QTPlayer", "恢复默认刷新率")
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-37
@@ -1,32 +1,59 @@
|
|||||||
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';
|
||||||
|
//import 'package:audio_service/audio_service.dart' as audio_service;
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'services/audio_service.dart';
|
import 'services/audio_service.dart';
|
||||||
import 'services/playback_service.dart';
|
import 'services/playback_service.dart';
|
||||||
import 'services/app_lifecycle_service.dart';
|
import 'services/app_lifecycle_service.dart';
|
||||||
import 'services/webdav_service.dart';
|
import 'services/webdav_service.dart';
|
||||||
|
import 'services/audio_player_handler.dart';
|
||||||
import 'pages/home_page.dart';
|
import 'pages/home_page.dart';
|
||||||
import 'pages/player_page.dart';
|
import 'pages/player_page.dart';
|
||||||
import 'pages/playlist_page.dart';
|
import 'pages/playlist_page.dart';
|
||||||
import 'widgets/global_mini_player.dart';
|
import 'widgets/global_mini_player.dart';
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
// ⏱️ 全局计时器
|
|
||||||
final stopwatch = Stopwatch();
|
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();
|
stopwatch.start();
|
||||||
debugPrint('⏱️ T0 main: ${stopwatch.elapsedMilliseconds}ms');
|
debugPrint('⏱️ T0 main: ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
debugPrint('⏱️ T0.5 ensureInitialized: ${stopwatch.elapsedMilliseconds}ms');
|
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(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => AudioService()),
|
ChangeNotifierProvider(create: (_) => AudioService()),
|
||||||
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
||||||
|
Provider<AudioPlayerHandler>(create: (_) => _audioHandler!),
|
||||||
],
|
],
|
||||||
child: const QTPlayerApp(),
|
child: const QTPlayerApp(),
|
||||||
),
|
),
|
||||||
@@ -35,13 +62,15 @@ void main() {
|
|||||||
debugPrint('⏱️ T0.8 runApp 完成: ${stopwatch.elapsedMilliseconds}ms');
|
debugPrint('⏱️ T0.8 runApp 完成: ${stopwatch.elapsedMilliseconds}ms');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 路由管理器(控制 MiniPlayer 显示/隐藏)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
class RouteManager extends ChangeNotifier {
|
class RouteManager extends ChangeNotifier {
|
||||||
static final RouteManager _instance = RouteManager._internal();
|
static final RouteManager _instance = RouteManager._internal();
|
||||||
factory RouteManager() => _instance;
|
factory RouteManager() => _instance;
|
||||||
RouteManager._internal();
|
RouteManager._internal();
|
||||||
|
|
||||||
String _currentRoute = '/';
|
String _currentRoute = '/';
|
||||||
|
|
||||||
String get currentRoute => _currentRoute;
|
String get currentRoute => _currentRoute;
|
||||||
|
|
||||||
bool get showMiniPlayer =>
|
bool get showMiniPlayer =>
|
||||||
@@ -56,6 +85,9 @@ class RouteManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 导航观察者(监听路由变化)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
||||||
void _updateRoute(Route? route) {
|
void _updateRoute(Route? route) {
|
||||||
final name = route?.settings.name ?? '/';
|
final name = route?.settings.name ?? '/';
|
||||||
@@ -78,6 +110,9 @@ class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 主应用
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
class QTPlayerApp extends StatefulWidget {
|
class QTPlayerApp extends StatefulWidget {
|
||||||
const QTPlayerApp({super.key});
|
const QTPlayerApp({super.key});
|
||||||
|
|
||||||
@@ -86,21 +121,123 @@ class QTPlayerApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _QTPlayerAppState extends State<QTPlayerApp> {
|
class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||||
|
bool _isInitializing = false;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 生命周期
|
||||||
|
// ============================================================
|
||||||
@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');
|
||||||
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
debugPrint('⏱️ T1 build: ${stopwatch.elapsedMilliseconds}ms');
|
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(
|
return MaterialApp(
|
||||||
title: '清听',
|
title: '清听',
|
||||||
theme: ThemeData(
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+76
-57
@@ -1,10 +1,17 @@
|
|||||||
|
// lib/pages/player_page.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
import '../main.dart';
|
|
||||||
|
|
||||||
class PlayerPage extends StatefulWidget {
|
class PlayerPage extends StatefulWidget {
|
||||||
const PlayerPage({super.key});
|
final VoidCallback? onClose;
|
||||||
|
final VoidCallback? onOpenPlaylist;
|
||||||
|
|
||||||
|
const PlayerPage({
|
||||||
|
super.key,
|
||||||
|
this.onClose,
|
||||||
|
this.onOpenPlaylist,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerPage> createState() => _PlayerPageState();
|
State<PlayerPage> createState() => _PlayerPageState();
|
||||||
@@ -20,6 +27,22 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final service = context.watch<AudioService>();
|
final service = context.watch<AudioService>();
|
||||||
@@ -28,33 +51,22 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
if (song == null) {
|
if (song == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0E1211),
|
backgroundColor: const Color(0xFF0E1211),
|
||||||
appBar: AppBar(
|
body: Center(
|
||||||
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(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.music_off, size: 64, color: Colors.grey),
|
const Text(
|
||||||
SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'没有正在播放的歌曲',
|
'没有正在播放的歌曲',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
ElevatedButton(
|
||||||
'请先在首页点击一首歌曲',
|
onPressed: _closePage,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
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 isPlaying = service.isPlaying;
|
||||||
final position = service.position;
|
|
||||||
final duration = service.duration;
|
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(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0E1211),
|
backgroundColor: const Color(0xFF0E1211),
|
||||||
@@ -85,7 +84,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: _closePage,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'正在播放',
|
'正在播放',
|
||||||
@@ -157,17 +156,41 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
Column(
|
_buildProgressSection(service, duration),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_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: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final width = constraints.maxWidth;
|
final width = constraints.maxWidth;
|
||||||
final bufferWidth =
|
final bufferWidth = width * bufferProgress.clamp(0.0, 1.0);
|
||||||
width * bufferProgress.clamp(0.0, 1.0);
|
final progressWidth = width * displayProgress.clamp(0.0, 1.0);
|
||||||
final progressWidth =
|
|
||||||
width * displayProgress.clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
@@ -220,8 +243,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
onChangeEnd: (value) {
|
onChangeEnd: (value) {
|
||||||
final newPosition = Duration(
|
final newPosition = Duration(
|
||||||
milliseconds:
|
milliseconds:
|
||||||
(value * duration.inMilliseconds)
|
(value * duration.inMilliseconds).round(),
|
||||||
.round(),
|
|
||||||
);
|
);
|
||||||
service.seekTo(newPosition);
|
service.seekTo(newPosition);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -256,9 +278,13 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const SizedBox(height: 32),
|
},
|
||||||
Row(
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControlButtons(AudioService service, bool isPlaying) {
|
||||||
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -281,8 +307,8 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFFB8D4D0),
|
color: const Color(0xFFB8D4D0),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -303,9 +329,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: _openPlaylist,
|
||||||
navigatorKey.currentState?.pushNamed('/playlist');
|
|
||||||
},
|
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.playlist_play_outlined,
|
Icons.playlist_play_outlined,
|
||||||
color: Color(0xFFB8D4D0),
|
color: Color(0xFFB8D4D0),
|
||||||
@@ -315,11 +339,6 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 'package:media_kit/media_kit.dart';
|
||||||
import 'playback_service.dart';
|
import 'playback_service.dart';
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 播放模式
|
|
||||||
// ============================================================
|
|
||||||
enum PlayMode {
|
enum PlayMode {
|
||||||
sequential, // 顺序循环
|
sequential,
|
||||||
repeatOne, // 单曲循环
|
repeatOne,
|
||||||
shuffle, // 随机播放
|
shuffle,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 歌曲模型
|
|
||||||
// ============================================================
|
|
||||||
class Song {
|
class Song {
|
||||||
final String id;
|
final String id;
|
||||||
final String title;
|
final String title;
|
||||||
@@ -30,49 +24,43 @@ class Song {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// AudioService - 播放状态管理与 UI 数据源
|
|
||||||
// ============================================================
|
|
||||||
class AudioService extends ChangeNotifier {
|
class AudioService extends ChangeNotifier {
|
||||||
// ---------- 单例 ----------
|
|
||||||
static final AudioService _instance = AudioService._internal();
|
static final AudioService _instance = AudioService._internal();
|
||||||
factory AudioService() => _instance;
|
factory AudioService() => _instance;
|
||||||
AudioService._internal();
|
AudioService._internal();
|
||||||
|
|
||||||
// ---------- 播放状态 ----------
|
// ---- 基础状态(低频,触发 UI 重建) ----
|
||||||
Song? _currentSong;
|
Song? _currentSong;
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
Duration _position = Duration.zero;
|
|
||||||
Duration _duration = Duration.zero;
|
|
||||||
Duration _bufferedPosition = Duration.zero;
|
|
||||||
|
|
||||||
// ---------- 播放模式 ----------
|
|
||||||
PlayMode _playMode = PlayMode.sequential;
|
PlayMode _playMode = PlayMode.sequential;
|
||||||
|
|
||||||
// ---------- 播放队列 ----------
|
// ---- 播放队列 ----
|
||||||
List<Song> _queue = [];
|
List<Song> _queue = [];
|
||||||
int _currentIndex = -1;
|
int _currentIndex = -1;
|
||||||
|
|
||||||
// ---------- 随机播放相关 ----------
|
|
||||||
List<int> _shuffledIndices = [];
|
List<int> _shuffledIndices = [];
|
||||||
int _shuffledIndex = -1;
|
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;
|
bool _listening = false;
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
// ---------- Getter ----------
|
// ---- Getter(高频字段不走 ChangeNotifier) ----
|
||||||
Song? get currentSong => _currentSong;
|
Song? get currentSong => _currentSong;
|
||||||
bool get isPlaying => _isPlaying;
|
bool get isPlaying => _isPlaying;
|
||||||
Duration get position => _position;
|
|
||||||
Duration get duration => _duration;
|
|
||||||
Duration get bufferedPosition => _bufferedPosition;
|
|
||||||
PlayMode get playMode => _playMode;
|
PlayMode get playMode => _playMode;
|
||||||
List<Song> get queue => List.unmodifiable(_queue);
|
List<Song> get queue => List.unmodifiable(_queue);
|
||||||
int get currentIndex => _currentIndex;
|
int get currentIndex => _currentIndex;
|
||||||
bool get hasQueue => _queue.isNotEmpty;
|
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 {
|
IconData get playModeIcon {
|
||||||
switch (_playMode) {
|
switch (_playMode) {
|
||||||
case PlayMode.sequential:
|
case PlayMode.sequential:
|
||||||
@@ -84,7 +72,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放模式切换 ----------
|
// ---- 切换播放模式 ----
|
||||||
void togglePlayMode() {
|
void togglePlayMode() {
|
||||||
switch (_playMode) {
|
switch (_playMode) {
|
||||||
case PlayMode.sequential:
|
case PlayMode.sequential:
|
||||||
@@ -100,7 +88,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 设置播放队列 ----------
|
// ---- 设置播放队列 ----
|
||||||
void setQueue(List<Song> queue, {int startIndex = 0}) {
|
void setQueue(List<Song> queue, {int startIndex = 0}) {
|
||||||
if (queue.isEmpty) {
|
if (queue.isEmpty) {
|
||||||
_clearQueue();
|
_clearQueue();
|
||||||
@@ -110,7 +98,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
_queue = List.from(queue);
|
_queue = List.from(queue);
|
||||||
_currentIndex = startIndex.clamp(0, _queue.length - 1);
|
_currentIndex = startIndex.clamp(0, _queue.length - 1);
|
||||||
|
|
||||||
// 初始化随机播放索引
|
|
||||||
_shuffledIndices = List.generate(_queue.length, (i) => i);
|
_shuffledIndices = List.generate(_queue.length, (i) => i);
|
||||||
_shuffledIndices.shuffle();
|
_shuffledIndices.shuffle();
|
||||||
_shuffledIndex = _shuffledIndices.indexOf(_currentIndex);
|
_shuffledIndex = _shuffledIndices.indexOf(_currentIndex);
|
||||||
@@ -119,11 +106,9 @@ class AudioService extends ChangeNotifier {
|
|||||||
_currentIndex = _shuffledIndices[0];
|
_currentIndex = _shuffledIndices[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放当前歌曲
|
|
||||||
_playCurrent();
|
_playCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 清空队列 ----------
|
|
||||||
void _clearQueue() {
|
void _clearQueue() {
|
||||||
_queue.clear();
|
_queue.clear();
|
||||||
_currentIndex = -1;
|
_currentIndex = -1;
|
||||||
@@ -132,18 +117,15 @@ class AudioService extends ChangeNotifier {
|
|||||||
stopPlay();
|
stopPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放指定歌曲(外部入口) ----------
|
// ---- 播放指定歌曲 ----
|
||||||
Future<void> playSong(Song song) async {
|
Future<void> playSong(Song song) async {
|
||||||
// 如果当前队列不包含这首歌,替换队列
|
|
||||||
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
||||||
setQueue([song], startIndex: 0);
|
setQueue([song], startIndex: 0);
|
||||||
} else {
|
} else {
|
||||||
// 如果已经在队列中,直接播放
|
|
||||||
_playCurrent();
|
_playCurrent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放当前索引歌曲 ----------
|
|
||||||
void _playCurrent() {
|
void _playCurrent() {
|
||||||
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
|
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
|
||||||
stopPlay();
|
stopPlay();
|
||||||
@@ -152,9 +134,11 @@ class AudioService extends ChangeNotifier {
|
|||||||
|
|
||||||
final song = _queue[_currentIndex];
|
final song = _queue[_currentIndex];
|
||||||
_currentSong = song;
|
_currentSong = song;
|
||||||
_position = Duration.zero;
|
|
||||||
_duration = Duration.zero;
|
// ⭐ 重置进度(用 ValueNotifier)
|
||||||
_bufferedPosition = Duration.zero;
|
positionNotifier.value = Duration.zero;
|
||||||
|
durationNotifier.value = Duration.zero;
|
||||||
|
bufferedNotifier.value = Duration.zero;
|
||||||
|
|
||||||
_startListening();
|
_startListening();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -163,15 +147,13 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 由 PlaybackService 实际播放,状态由流更新
|
|
||||||
PlaybackService().play(song.url!);
|
PlaybackService().play(song.url!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放下一首 ----------
|
// ---- 下一首 ----
|
||||||
void next() {
|
void next() {
|
||||||
if (_queue.isEmpty) return;
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
// 随机模式
|
|
||||||
if (_playMode == PlayMode.shuffle) {
|
if (_playMode == PlayMode.shuffle) {
|
||||||
if (_shuffledIndices.isEmpty) return;
|
if (_shuffledIndices.isEmpty) return;
|
||||||
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
|
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
|
||||||
@@ -181,17 +163,15 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 顺序/单曲模式
|
|
||||||
final nextIdx = (_currentIndex + 1) % _queue.length;
|
final nextIdx = (_currentIndex + 1) % _queue.length;
|
||||||
_currentIndex = nextIdx;
|
_currentIndex = nextIdx;
|
||||||
_playCurrent();
|
_playCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放上一首 ----------
|
// ---- 上一首 ----
|
||||||
void previous() {
|
void previous() {
|
||||||
if (_queue.isEmpty) return;
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
// 随机模式
|
|
||||||
if (_playMode == PlayMode.shuffle) {
|
if (_playMode == PlayMode.shuffle) {
|
||||||
if (_shuffledIndices.isEmpty) return;
|
if (_shuffledIndices.isEmpty) return;
|
||||||
final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length;
|
final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length;
|
||||||
@@ -205,7 +185,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 顺序/单曲模式
|
|
||||||
final prevIdx = (_currentIndex - 1) % _queue.length;
|
final prevIdx = (_currentIndex - 1) % _queue.length;
|
||||||
if (prevIdx < 0) {
|
if (prevIdx < 0) {
|
||||||
_currentIndex = _queue.length - 1;
|
_currentIndex = _queue.length - 1;
|
||||||
@@ -215,7 +194,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
_playCurrent();
|
_playCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放/暂停切换 ----------
|
// ---- 播放/暂停 ----
|
||||||
void togglePlay() {
|
void togglePlay() {
|
||||||
if (_currentSong == null) return;
|
if (_currentSong == null) return;
|
||||||
|
|
||||||
@@ -224,39 +203,21 @@ class AudioService extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
PlaybackService().resume();
|
PlaybackService().resume();
|
||||||
}
|
}
|
||||||
// 状态由 player.stream.playing 更新
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 停止播放 ----------
|
|
||||||
void stopPlay() {
|
void stopPlay() {
|
||||||
_currentSong = null;
|
_currentSong = null;
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_position = Duration.zero;
|
positionNotifier.value = Duration.zero;
|
||||||
_duration = Duration.zero;
|
durationNotifier.value = Duration.zero;
|
||||||
_bufferedPosition = Duration.zero;
|
bufferedNotifier.value = Duration.zero;
|
||||||
_stopListening();
|
_stopListening();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Seek ----------
|
|
||||||
void seekTo(Duration position) {
|
void seekTo(Duration position) {
|
||||||
PlaybackService().seek(position);
|
PlaybackService().seek(position);
|
||||||
_position = position;
|
positionNotifier.value = position;
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 处理播放结束 ----------
|
|
||||||
void _onPlaybackCompleted() {
|
|
||||||
if (_queue.isEmpty) return;
|
|
||||||
|
|
||||||
// 单曲循环模式
|
|
||||||
if (_playMode == PlayMode.repeatOne) {
|
|
||||||
_playCurrent();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他模式:播放下一首
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearQueue() {
|
void clearQueue() {
|
||||||
@@ -268,7 +229,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 监听 media_kit 状态 ----------
|
// ---- 监听 media_kit 状态 ----
|
||||||
void _startListening() {
|
void _startListening() {
|
||||||
if (_listening) return;
|
if (_listening) return;
|
||||||
_listening = true;
|
_listening = true;
|
||||||
@@ -279,39 +240,30 @@ class AudioService extends ChangeNotifier {
|
|||||||
player.stream.playing.listen((playing) {
|
player.stream.playing.listen((playing) {
|
||||||
if (_isPlaying != playing) {
|
if (_isPlaying != playing) {
|
||||||
_isPlaying = playing;
|
_isPlaying = playing;
|
||||||
notifyListeners();
|
notifyListeners(); // 只有播放状态变化才刷新
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ⭐ 进度更新:只更新 ValueNotifier,不触发全局重建
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.stream.position.listen((position) {
|
player.stream.position.listen((position) {
|
||||||
if (_position != position) {
|
positionNotifier.value = position;
|
||||||
_position = position;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.stream.duration.listen((duration) {
|
player.stream.duration.listen((duration) {
|
||||||
if (_duration != duration) {
|
durationNotifier.value = duration;
|
||||||
_duration = duration;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.stream.buffer.listen((buffer) {
|
player.stream.buffer.listen((buffer) {
|
||||||
if (_bufferedPosition != buffer) {
|
bufferedNotifier.value = buffer;
|
||||||
_bufferedPosition = buffer;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ 播放结束监听
|
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.stream.completed.listen((_) {
|
player.stream.completed.listen((_) {
|
||||||
_onPlaybackCompleted();
|
_onPlaybackCompleted();
|
||||||
@@ -327,9 +279,23 @@ class AudioService extends ChangeNotifier {
|
|||||||
_subscriptions.clear();
|
_subscriptions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 资源释放 ----------
|
void _onPlaybackCompleted() {
|
||||||
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
|
if (_playMode == PlayMode.repeatOne) {
|
||||||
|
_playCurrent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_stopListening();
|
_stopListening();
|
||||||
|
positionNotifier.dispose();
|
||||||
|
durationNotifier.dispose();
|
||||||
|
bufferedNotifier.dispose();
|
||||||
PlaybackService().dispose();
|
PlaybackService().dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// lib/widgets/global_mini_player.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
@@ -8,15 +9,16 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final audioService = context.watch<AudioService>();
|
// ⭐ 用 Selector 只监听 currentSong(低频变化)
|
||||||
final song = audioService.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;
|
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
// 底层遮罩
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -26,7 +28,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
color: const Color(0xFF1A1F1E),
|
color: const Color(0xFF1A1F1E),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 上层主体
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -38,7 +39,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
color: const Color(0xFF1A1F1E),
|
color: const Color(0xFF1A1F1E),
|
||||||
// ⭐ 用 Material 包裹 InkWell,获得更好的点击反馈
|
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -47,7 +47,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
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),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -81,7 +80,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
// ⭐ 强制去掉下划线
|
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@@ -94,7 +92,6 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
color: song != null
|
color: song != null
|
||||||
? Colors.grey[400]
|
? Colors.grey[400]
|
||||||
: Colors.grey[600],
|
: Colors.grey[600],
|
||||||
// ⭐ 强制去掉下划线
|
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@@ -105,7 +102,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
song != null && audioService.isPlaying
|
song != null && isPlaying
|
||||||
? Icons.pause
|
? Icons.pause
|
||||||
: Icons.play_arrow,
|
: Icons.play_arrow,
|
||||||
color: song != null ? Colors.white : Colors.grey[600],
|
color: song != null ? Colors.white : Colors.grey[600],
|
||||||
@@ -113,7 +110,7 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
audioService.togglePlay();
|
context.read<AudioService>().togglePlay();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+264
@@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.9"
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -17,6 +25,38 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.1"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -118,6 +166,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -136,6 +192,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -160,6 +224,38 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -192,6 +288,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
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:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -288,6 +392,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -296,6 +416,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -320,6 +464,54 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
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:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -360,6 +552,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
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:
|
safe_local_storage:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -437,6 +653,46 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.2"
|
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:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -557,6 +813,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
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:
|
sdks:
|
||||||
dart: ">=3.12.0 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: ">=3.44.0"
|
flutter: ">=3.44.0"
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ dependencies:
|
|||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
dio: ^5.4.0 # HTTP 客户端
|
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.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Reference in New Issue
Block a user