From f94cda4234b84cc6ddfe56e26743e4f19dde2227 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Thu, 20 Aug 2026 22:07:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E9=85=8D=E7=BD=AE=E6=9A=82?= =?UTF-8?q?=E6=97=B6=E5=88=B0=E6=AD=A4=EF=BC=8C=E5=90=8E=E5=8F=B0=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E9=97=AE=E9=A2=98=E5=B9=B6=E5=85=A5=E7=94=9F=E5=91=BD?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E7=AE=A1=E7=90=86=E5=90=8C=E6=AD=A5=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 15 +- .../com/example/qt_player/MainActivity.kt | 32 +- lib/main.dart | 179 ++++++-- lib/pages/player_page.dart | 409 +++++++++--------- lib/services/audio_player_handler.dart | 90 ++++ lib/services/audio_service.dart | 138 +++--- lib/widgets/global_mini_player.dart | 17 +- pubspec.lock | 264 +++++++++++ pubspec.yaml | 7 +- 9 files changed, 787 insertions(+), 364 deletions(-) create mode 100644 lib/services/audio_player_handler.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c94c79f..b45c06c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,19 +3,18 @@ - + - - - - + + + android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true" + android:foregroundServiceType="mediaPlayback"> + diff --git a/android/app/src/main/kotlin/com/example/qt_player/MainActivity.kt b/android/app/src/main/kotlin/com/example/qt_player/MainActivity.kt index 28b6fb9..3088f1d 100644 --- a/android/app/src/main/kotlin/com/example/qt_player/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/qt_player/MainActivity.kt @@ -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", "恢复默认刷新率") } diff --git a/lib/main.dart b/lib/main.dart index 705feea..3fd872c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 navigatorKey = GlobalKey(); - -// ⏱️ 全局计时器 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(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 { + 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 _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 _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 _initWebDAV() async { + try { + final hasCred = await WebDAVService.instance.loadCredentials(); + debugPrint('✅ WebDAV 加载完成, 已连接: $hasCred'); + } catch (e) { + debugPrint('❌ WebDAV 加载失败: $e'); + } + } + + Future _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(); + final handler = context.read(); + 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 { ); } } - -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); - } -} diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index 5e0fa93..f20223a 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -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 createState() => _PlayerPageState(); @@ -20,6 +27,22 @@ class _PlayerPageState extends State { 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(); @@ -28,33 +51,22 @@ class _PlayerPageState extends State { 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 { } 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 { 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 { ], ), 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), + ], + ); + } } diff --git a/lib/services/audio_player_handler.dart b/lib/services/audio_player_handler.dart new file mode 100644 index 0000000..d6a0669 --- /dev/null +++ b/lib/services/audio_player_handler.dart @@ -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 play() async { + _playback.resume(); + } + + @override + Future pause() async { + _playback.pause(); + } + + @override + Future stop() async { + _playback.stop(); + } + + @override + Future seek(Duration position) async { + _playback.seek(position); + } + + @override + Future skipToNext() async { + _localAudio.next(); + } + + @override + Future skipToPrevious() async { + _localAudio.previous(); + } + + @override + Future click([MediaButton button = MediaButton.media]) async { + // 默认行为:打开 App + } +} diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 06cc89b..94319c8 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -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 _queue = []; int _currentIndex = -1; - - // ---------- 随机播放相关 ---------- List _shuffledIndices = []; int _shuffledIndex = -1; - // ---------- 监听控制 ---------- + // ---- ⭐ 高频进度(用 ValueNotifier,不触发全局重建) ---- + final ValueNotifier positionNotifier = ValueNotifier(Duration.zero); + final ValueNotifier durationNotifier = ValueNotifier(Duration.zero); + final ValueNotifier bufferedNotifier = ValueNotifier(Duration.zero); + bool _listening = false; final List _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 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 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 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(); } diff --git a/lib/widgets/global_mini_player.dart b/lib/widgets/global_mini_player.dart index b6e6d75..5924a17 100644 --- a/lib/widgets/global_mini_player.dart +++ b/lib/widgets/global_mini_player.dart @@ -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(); - final song = audioService.currentSong; + // ⭐ 用 Selector 只监听 currentSong(低频变化) + final song = context.select((s) => s.currentSong); + // ⭐ 只监听播放状态(低频变化) + final isPlaying = context.select((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().togglePlay(); } }, ), diff --git a/pubspec.lock b/pubspec.lock index 50d2edf..65a8926 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 747358e..575f988 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.