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.