Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40d4af322b | ||
|
|
1aba004e43 | ||
|
|
cdf77b308d | ||
|
|
200067a3b2 |
@@ -7,6 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
@@ -8,6 +8,8 @@ import io.flutter.embedding.android.FlutterActivity
|
|||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioManager
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
@@ -32,9 +34,32 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
requestAudioFocus()
|
||||||
|
|
||||||
setHighRefreshRate()
|
setHighRefreshRate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requestAudioFocus() {
|
||||||
|
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
audioManager.requestAudioFocus(
|
||||||
|
AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||||
|
Log.d("MainActivity", "音频焦点变化: $focusChange")
|
||||||
|
},
|
||||||
|
AudioManager.STREAM_MUSIC,
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
audioManager.requestAudioFocus(
|
||||||
|
null,
|
||||||
|
AudioManager.STREAM_MUSIC,
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d("MainActivity", "✅ 已请求音频焦点")
|
||||||
|
}
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "9.0.1" apply false
|
id("com.android.application") version "9.0.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// lib/audio/player_controller.dart
|
|
||||||
import '../services/playback_service.dart'; // ⭐ 修正路径
|
|
||||||
|
|
||||||
class PlayerController {
|
|
||||||
static final PlayerController _instance = PlayerController._internal();
|
|
||||||
factory PlayerController() => _instance;
|
|
||||||
PlayerController._internal();
|
|
||||||
|
|
||||||
final PlaybackService _playback = PlaybackService();
|
|
||||||
|
|
||||||
// ---- 状态流(只读) ----
|
|
||||||
Stream<bool> get playingStream => _playback.player.stream.playing;
|
|
||||||
Stream<Duration> get positionStream => _playback.player.stream.position;
|
|
||||||
Stream<Duration> get durationStream => _playback.player.stream.duration;
|
|
||||||
Stream<void> get completedStream => _playback.player.stream.completed;
|
|
||||||
|
|
||||||
// ---- 当前状态快照 ----
|
|
||||||
bool get isPlaying => _playback.player.state.playing;
|
|
||||||
Duration get position => _playback.player.state.position;
|
|
||||||
Duration get duration => _playback.player.state.duration;
|
|
||||||
|
|
||||||
// ---- 控制命令 ----
|
|
||||||
Future<void> play() => _playback.resume();
|
|
||||||
Future<void> pause() => _playback.pause();
|
|
||||||
Future<void> stop() => _playback.stop();
|
|
||||||
Future<void> seek(Duration position) => _playback.seek(position);
|
|
||||||
|
|
||||||
// ---- 加载歌曲 ----
|
|
||||||
void load(String url, {Map<String, String>? headers}) {
|
|
||||||
_playback.play(url, headers: headers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+69
-57
@@ -5,6 +5,7 @@ import 'package:media_kit/media_kit.dart';
|
|||||||
import 'package:audio_service/audio_service.dart' as audio_service;
|
import 'package:audio_service/audio_service.dart' as audio_service;
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
import 'package:flutter/services.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';
|
||||||
@@ -15,8 +16,8 @@ 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';
|
||||||
|
import 'utils/navigation.dart';
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|
||||||
final stopwatch = Stopwatch();
|
final stopwatch = Stopwatch();
|
||||||
|
|
||||||
bool _appInitialized = false;
|
bool _appInitialized = false;
|
||||||
@@ -53,6 +54,8 @@ void main() async {
|
|||||||
try {
|
try {
|
||||||
_audioHandler = AudioPlayerHandler();
|
_audioHandler = AudioPlayerHandler();
|
||||||
debugPrint('⏱️ T0.62 AudioPlayerHandler created');
|
debugPrint('⏱️ T0.62 AudioPlayerHandler created');
|
||||||
|
// ⭐ 将 Handler 注入到 AudioService
|
||||||
|
AudioService().setHandler(_audioHandler!);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint('❌ AudioPlayerHandler failed: $e');
|
debugPrint('❌ AudioPlayerHandler failed: $e');
|
||||||
debugPrint('$st');
|
debugPrint('$st');
|
||||||
@@ -363,65 +366,74 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
return MaterialApp(
|
// ⭐ 使用 AnnotatedRegion 包裹 MaterialApp,统一控制系统栏样式
|
||||||
title: '清听',
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
theme: ThemeData(
|
value: SystemUiOverlayStyle(
|
||||||
brightness: Brightness.dark,
|
statusBarColor: Colors.transparent,
|
||||||
primaryColor: const Color(0xFF7C9A9E),
|
statusBarIconBrightness: Brightness.light,
|
||||||
colorScheme: const ColorScheme.dark(
|
systemNavigationBarColor: const Color(0xFF1A1F1E),
|
||||||
primary: Color(0xFF7C9A9E),
|
systemNavigationBarIconBrightness: Brightness.light,
|
||||||
secondary: Color(0xFFB8D4D0),
|
|
||||||
surface: Color(0xFF1A1F1E),
|
|
||||||
onSurface: Colors.white,
|
|
||||||
),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
),
|
||||||
navigatorKey: navigatorKey,
|
child: MaterialApp(
|
||||||
navigatorObservers: [MiniPlayerNavigatorObserver()],
|
title: '清听',
|
||||||
onGenerateRoute: _onGenerateRoute,
|
theme: ThemeData(
|
||||||
builder: (context, child) {
|
brightness: Brightness.dark,
|
||||||
return Stack(
|
primaryColor: const Color(0xFF7C9A9E),
|
||||||
fit: StackFit.expand,
|
colorScheme: const ColorScheme.dark(
|
||||||
children: [
|
primary: Color(0xFF7C9A9E),
|
||||||
if (child != null) child,
|
secondary: Color(0xFFB8D4D0),
|
||||||
AnimatedBuilder(
|
surface: Color(0xFF1A1F1E),
|
||||||
animation: RouteManager(),
|
onSurface: Colors.white,
|
||||||
builder: (context, _) {
|
),
|
||||||
final routeManager = RouteManager();
|
useMaterial3: true,
|
||||||
final shouldShow = routeManager.currentRoute != '/player' &&
|
),
|
||||||
routeManager.currentRoute != '/playlist';
|
navigatorKey: navigatorKey,
|
||||||
|
navigatorObservers: [MiniPlayerNavigatorObserver()],
|
||||||
|
onGenerateRoute: _onGenerateRoute,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (child != null) child,
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: RouteManager(),
|
||||||
|
builder: (context, _) {
|
||||||
|
final routeManager = RouteManager();
|
||||||
|
final shouldShow = routeManager.currentRoute != '/player' &&
|
||||||
|
routeManager.currentRoute != '/playlist';
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
switchInCurve: Curves.easeOut,
|
switchInCurve: Curves.easeOut,
|
||||||
switchOutCurve: Curves.easeIn,
|
switchOutCurve: Curves.easeIn,
|
||||||
transitionBuilder: (child, animation) {
|
transitionBuilder: (child, animation) {
|
||||||
return FadeTransition(
|
return FadeTransition(
|
||||||
opacity: animation,
|
opacity: animation,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: Tween<Offset>(
|
position: Tween<Offset>(
|
||||||
begin: const Offset(0, 0.1),
|
begin: const Offset(0, 0.1),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(animation),
|
).animate(animation),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: shouldShow
|
|
||||||
? const Align(
|
|
||||||
key: ValueKey('mini_player_visible'),
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: GlobalMiniPlayer(),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(
|
|
||||||
key: ValueKey('mini_player_hidden'),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
child: shouldShow
|
||||||
],
|
? const Align(
|
||||||
);
|
key: ValueKey('mini_player_visible'),
|
||||||
},
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: GlobalMiniPlayer(),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(
|
||||||
|
key: ValueKey('mini_player_hidden'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,49 +4,62 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:audio_service/audio_service.dart' as audio_service;
|
import 'package:audio_service/audio_service.dart' as audio_service;
|
||||||
import '../audio/player_controller.dart';
|
|
||||||
import '../audio/playback_state_manager.dart';
|
|
||||||
import '../services/audio_service.dart';
|
|
||||||
import '../utils/file_provider_utils.dart';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'playback_service.dart';
|
||||||
|
import 'audio_service.dart';
|
||||||
|
import '../utils/file_provider_utils.dart';
|
||||||
|
|
||||||
|
/// 音频服务处理器:连接 media_kit 播放引擎与 Android 系统媒体会话
|
||||||
|
/// 职责:发布播放状态到系统通知栏,接收系统媒体按钮事件
|
||||||
|
/// 它不拥有 Player 实例,所有播放控制均委托给 PlaybackService
|
||||||
class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||||
final PlayerController _player = PlayerController();
|
final PlaybackService _playback = PlaybackService();
|
||||||
final PlaybackStateManager _state = PlaybackStateManager();
|
|
||||||
|
|
||||||
|
// 当前媒体信息
|
||||||
String? _currentId;
|
String? _currentId;
|
||||||
String? _currentTitle;
|
String? _currentTitle;
|
||||||
String? _currentArtist;
|
String? _currentArtist;
|
||||||
|
|
||||||
Duration _currentPosition = Duration.zero;
|
Duration _currentPosition = Duration.zero;
|
||||||
DateTime _lastPublishTime = DateTime.now();
|
DateTime _lastPublishTime = DateTime.now();
|
||||||
static const Duration _publishInterval = Duration(milliseconds: 500);
|
static const Duration _publishInterval = Duration(milliseconds: 500);
|
||||||
|
|
||||||
// ⭐ 缓存 artwork 文件路径,避免重复写入
|
|
||||||
String? _currentArtworkPath;
|
String? _currentArtworkPath;
|
||||||
|
bool _isPublishing = false;
|
||||||
|
|
||||||
AudioPlayerHandler() {
|
AudioPlayerHandler() {
|
||||||
_player.playingStream.listen((playing) {
|
_bindToPlayer();
|
||||||
_state.updatePlaying(playing);
|
}
|
||||||
_publishState();
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 绑定播放器状态流
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
void _bindToPlayer() {
|
||||||
|
final player = _playback.player;
|
||||||
|
|
||||||
|
player.stream.playing.listen((playing) {
|
||||||
|
_publishPlaybackState(
|
||||||
|
playing: playing,
|
||||||
|
position: _currentPosition,
|
||||||
|
duration: player.state.duration,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
_player.positionStream.listen((position) {
|
player.stream.position.listen((position) {
|
||||||
_currentPosition = position;
|
_currentPosition = position;
|
||||||
_state.updatePosition(position);
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (now.difference(_lastPublishTime) >= _publishInterval) {
|
if (now.difference(_lastPublishTime) >= _publishInterval) {
|
||||||
_lastPublishTime = now;
|
_lastPublishTime = now;
|
||||||
// ⭐ 只保留 _publishStateOnly()
|
_publishPlaybackState(
|
||||||
_publishStateOnly();
|
playing: player.state.playing,
|
||||||
|
position: position,
|
||||||
|
duration: player.state.duration,
|
||||||
|
onlyPosition: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_player.durationStream.listen((duration) {
|
player.stream.duration.listen((duration) {
|
||||||
debugPrint('🎯 [durationStream] duration=$duration');
|
|
||||||
_state.updateDuration(duration);
|
|
||||||
if (_currentId != null && _currentTitle != null) {
|
if (_currentId != null && _currentTitle != null) {
|
||||||
_updateMediaItem(
|
_updateMediaItem(
|
||||||
id: _currentId!,
|
id: _currentId!,
|
||||||
@@ -55,45 +68,93 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_publishState();
|
_publishPlaybackState(
|
||||||
|
playing: player.state.playing,
|
||||||
|
position: _currentPosition,
|
||||||
|
duration: duration,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 发布状态 ----
|
// ═══════════════════════════════════════════════════════════════
|
||||||
void _publishState() {
|
// 外部同步接口(由 AudioService 调用)
|
||||||
final state = _state.playbackState;
|
// ═══════════════════════════════════════════════════════════════
|
||||||
playbackState.add(audio_service.PlaybackState(
|
void syncState(Song? song) {
|
||||||
controls: state.controls,
|
if (song == null) {
|
||||||
processingState: state.processingState,
|
_currentId = null;
|
||||||
playing: state.playing,
|
_currentTitle = null;
|
||||||
androidCompactActionIndices: state.androidCompactActionIndices,
|
_currentArtist = null;
|
||||||
updatePosition: _currentPosition,
|
return;
|
||||||
updateTime: DateTime.now(),
|
}
|
||||||
systemActions: const {
|
|
||||||
audio_service.MediaAction.seek,
|
_updateMediaItem(
|
||||||
},
|
id: song.id,
|
||||||
));
|
title: song.title,
|
||||||
|
artist: song.artist,
|
||||||
|
artwork: song.artwork,
|
||||||
|
);
|
||||||
|
|
||||||
|
final player = _playback.player;
|
||||||
|
_publishPlaybackState(
|
||||||
|
playing: player.state.playing,
|
||||||
|
position: _currentPosition,
|
||||||
|
duration: player.state.duration,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _publishStateOnly() {
|
// ═══════════════════════════════════════════════════════════════
|
||||||
final current = playbackState.value;
|
// 状态发布
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
void _publishPlaybackState({
|
||||||
|
required bool playing,
|
||||||
|
required Duration position,
|
||||||
|
required Duration duration,
|
||||||
|
bool onlyPosition = false,
|
||||||
|
}) {
|
||||||
|
if (_isPublishing) return;
|
||||||
|
_isPublishing = true;
|
||||||
|
|
||||||
playbackState.add(audio_service.PlaybackState(
|
try {
|
||||||
controls: current.controls.isNotEmpty
|
final controls = _buildControls(playing);
|
||||||
? current.controls
|
final state = audio_service.PlaybackState(
|
||||||
: _state.playbackState.controls,
|
controls: controls,
|
||||||
processingState: _state.playbackState.processingState,
|
processingState: duration.inMilliseconds > 0
|
||||||
playing: current.playing,
|
? audio_service.AudioProcessingState.ready
|
||||||
androidCompactActionIndices: current.androidCompactActionIndices,
|
: audio_service.AudioProcessingState.idle,
|
||||||
updatePosition: _currentPosition,
|
playing: playing,
|
||||||
updateTime: DateTime.now(),
|
updatePosition: position,
|
||||||
systemActions: const {
|
updateTime: DateTime.now(),
|
||||||
audio_service.MediaAction.seek,
|
systemActions: const {audio_service.MediaAction.seek},
|
||||||
},
|
);
|
||||||
));
|
playbackState.add(state);
|
||||||
|
} finally {
|
||||||
|
_isPublishing = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 更新媒体信息 ----
|
List<audio_service.MediaControl> _buildControls(bool playing) {
|
||||||
|
return [
|
||||||
|
const audio_service.MediaControl(
|
||||||
|
androidIcon: 'drawable/ic_previous',
|
||||||
|
label: '上一曲',
|
||||||
|
action: audio_service.MediaAction.skipToPrevious,
|
||||||
|
),
|
||||||
|
audio_service.MediaControl(
|
||||||
|
androidIcon: playing ? 'drawable/ic_pause' : 'drawable/ic_play',
|
||||||
|
label: playing ? '暂停' : '播放',
|
||||||
|
action: audio_service.MediaAction.playPause,
|
||||||
|
),
|
||||||
|
const audio_service.MediaControl(
|
||||||
|
androidIcon: 'drawable/ic_next',
|
||||||
|
label: '下一曲',
|
||||||
|
action: audio_service.MediaAction.skipToNext,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 媒体信息更新
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
void _updateMediaItem({
|
void _updateMediaItem({
|
||||||
required String id,
|
required String id,
|
||||||
required String title,
|
required String title,
|
||||||
@@ -101,21 +162,15 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
|||||||
Duration? duration,
|
Duration? duration,
|
||||||
Uint8List? artwork,
|
Uint8List? artwork,
|
||||||
}) {
|
}) {
|
||||||
debugPrint(
|
|
||||||
'📢 [handler] _updateMediaItem: artwork is ${artwork != null ? 'not null (${artwork.length} bytes)' : 'null'}');
|
|
||||||
_currentId = id;
|
_currentId = id;
|
||||||
_currentTitle = title;
|
_currentTitle = title;
|
||||||
_currentArtist = artist;
|
_currentArtist = artist;
|
||||||
final position = _player.position;
|
|
||||||
|
|
||||||
debugPrint('📢 [handler] updateMediaItem: $title - $artist');
|
final position = _playback.player.state.position;
|
||||||
|
|
||||||
// ⭐ 异步处理 artwork(不阻塞主流程)
|
|
||||||
_handleArtwork(id, artwork).then((artUri) {
|
_handleArtwork(id, artwork).then((artUri) {
|
||||||
// 如果 artUri 变化,重新推送 MediaItem
|
|
||||||
final current = mediaItem.value;
|
final current = mediaItem.value;
|
||||||
if (current != null && current.artUri != artUri) {
|
if (current != null && current.artUri != artUri) {
|
||||||
debugPrint('📢 [handler] updating artUri: $artUri');
|
|
||||||
mediaItem.add(audio_service.MediaItem(
|
mediaItem.add(audio_service.MediaItem(
|
||||||
id: current.id,
|
id: current.id,
|
||||||
title: current.title,
|
title: current.title,
|
||||||
@@ -127,38 +182,28 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 先推送不带封面图的 MediaItem(让 UI 尽快显示)
|
|
||||||
mediaItem.add(audio_service.MediaItem(
|
mediaItem.add(audio_service.MediaItem(
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
artist: artist,
|
artist: artist,
|
||||||
duration: duration ?? _player.duration,
|
duration: duration ?? _playback.player.state.duration,
|
||||||
extras: {'position': position.inMilliseconds},
|
extras: {'position': position.inMilliseconds},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理封面图:保存到本地并生成 content URI
|
|
||||||
Future<Uri?> _handleArtwork(String id, Uint8List? artwork) async {
|
Future<Uri?> _handleArtwork(String id, Uint8List? artwork) async {
|
||||||
if (artwork == null || artwork.isEmpty) {
|
if (artwork == null || artwork.isEmpty) return null;
|
||||||
_currentArtworkPath = null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
final artworkDir = Directory('${dir.path}/artworks');
|
final artworkDir = Directory('${dir.path}/artworks');
|
||||||
if (!await artworkDir.exists()) {
|
if (!await artworkDir.exists()) {
|
||||||
await artworkDir.create(recursive: true);
|
await artworkDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 md5 生成安全的文件名
|
|
||||||
final bytes = utf8.encode(id);
|
final bytes = utf8.encode(id);
|
||||||
final digest = md5.convert(bytes);
|
final digest = md5.convert(bytes);
|
||||||
final fileName = '$digest.jpg';
|
final fileName = '$digest.jpg';
|
||||||
final path = '${artworkDir.path}/$fileName';
|
final path = '${artworkDir.path}/$fileName';
|
||||||
final file = File(path);
|
final file = File(path);
|
||||||
|
|
||||||
// 检查文件是否已存在
|
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
final existingBytes = await file.readAsBytes();
|
final existingBytes = await file.readAsBytes();
|
||||||
if (existingBytes.length == artwork.length &&
|
if (existingBytes.length == artwork.length &&
|
||||||
@@ -167,12 +212,8 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
|||||||
return await FileProviderUtils.getContentUri(file);
|
return await FileProviderUtils.getContentUri(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入新文件
|
|
||||||
await file.writeAsBytes(artwork);
|
await file.writeAsBytes(artwork);
|
||||||
_currentArtworkPath = path;
|
_currentArtworkPath = path;
|
||||||
debugPrint('📢 [handler] artwork saved: $path');
|
|
||||||
|
|
||||||
return await FileProviderUtils.getContentUri(file);
|
return await FileProviderUtils.getContentUri(file);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ [handler] artwork handling failed: $e');
|
debugPrint('⚠️ [handler] artwork handling failed: $e');
|
||||||
@@ -180,50 +221,54 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 外部接口 ----
|
|
||||||
void updateNotification({
|
void updateNotification({
|
||||||
required String id,
|
required String id,
|
||||||
required String title,
|
required String title,
|
||||||
required String artist,
|
required String artist,
|
||||||
Uint8List? artwork,
|
Uint8List? artwork,
|
||||||
}) {
|
}) {
|
||||||
_updateMediaItem(
|
_updateMediaItem(id: id, title: title, artist: artist, artwork: artwork);
|
||||||
id: id,
|
final player = _playback.player;
|
||||||
title: title,
|
_publishPlaybackState(
|
||||||
artist: artist,
|
playing: player.state.playing,
|
||||||
artwork: artwork,
|
position: _currentPosition,
|
||||||
|
duration: player.state.duration,
|
||||||
);
|
);
|
||||||
_publishState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 控制命令 ----
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// audio_service 控制命令(全部委托给 PlaybackService)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@override
|
@override
|
||||||
Future<void> play() async {
|
Future<void> play() async {
|
||||||
debugPrint('▶️ [handler] play() called');
|
debugPrint('▶️ [handler] play() called');
|
||||||
_player.play();
|
await _playback.resume();
|
||||||
_publishState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> pause() async {
|
Future<void> pause() async {
|
||||||
debugPrint('⏸️ [handler] pause() called');
|
debugPrint('⏸️ [handler] pause() called');
|
||||||
_player.pause();
|
await _playback.pause();
|
||||||
_publishState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
debugPrint('⏹️ [handler] stop() called');
|
debugPrint('⏹️ [handler] stop() called');
|
||||||
_player.stop();
|
await _playback.stop();
|
||||||
_publishState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> seek(Duration position) async {
|
Future<void> seek(Duration position) async {
|
||||||
debugPrint('⏩ [handler] seek() called: $position');
|
debugPrint('⏩ [handler] seek() called: $position');
|
||||||
await _player.seek(position);
|
await _playback.seek(position);
|
||||||
_currentPosition = position;
|
_currentPosition = position;
|
||||||
_publishStateOnly();
|
final player = _playback.player;
|
||||||
|
_publishPlaybackState(
|
||||||
|
playing: player.state.playing,
|
||||||
|
position: position,
|
||||||
|
duration: player.state.duration,
|
||||||
|
onlyPosition: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -251,14 +296,14 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
|||||||
await skipToPrevious();
|
await skipToPrevious();
|
||||||
break;
|
break;
|
||||||
case audio_service.MediaButton.media:
|
case audio_service.MediaButton.media:
|
||||||
if (_player.isPlaying) {
|
if (_playback.player.state.playing) {
|
||||||
await pause();
|
await pause();
|
||||||
} else {
|
} else {
|
||||||
await play();
|
await play();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (_player.isPlaying) {
|
if (_playback.player.state.playing) {
|
||||||
await pause();
|
await pause();
|
||||||
} else {
|
} else {
|
||||||
await play();
|
await play();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import '../utils/artwork_helper.dart';
|
|||||||
import '../repositories/playlist_repository.dart';
|
import '../repositories/playlist_repository.dart';
|
||||||
import '../models/playlist.dart';
|
import '../models/playlist.dart';
|
||||||
import 'webdav_service.dart';
|
import 'webdav_service.dart';
|
||||||
|
import 'audio_player_handler.dart';
|
||||||
|
|
||||||
enum PlayMode {
|
enum PlayMode {
|
||||||
sequential,
|
sequential,
|
||||||
@@ -62,6 +63,9 @@ class AudioService extends ChangeNotifier {
|
|||||||
// ---- 当前播放的歌单 ID ----
|
// ---- 当前播放的歌单 ID ----
|
||||||
String? _currentPlaylistId;
|
String? _currentPlaylistId;
|
||||||
|
|
||||||
|
// ⭐ 插入位置:在现有成员变量之后,方法之前
|
||||||
|
AudioPlayerHandler? _handler; // ⭐ 添加这一行
|
||||||
|
|
||||||
// ---- 高频进度 ----
|
// ---- 高频进度 ----
|
||||||
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
|
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
|
||||||
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
|
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
|
||||||
@@ -115,6 +119,11 @@ class AudioService extends ChangeNotifier {
|
|||||||
_onSongChanged = callback;
|
_onSongChanged = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setHandler(AudioPlayerHandler handler) {
|
||||||
|
// ⭐ 添加这个方法
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
void togglePlayMode() {
|
void togglePlayMode() {
|
||||||
switch (_playMode) {
|
switch (_playMode) {
|
||||||
case PlayMode.sequential:
|
case PlayMode.sequential:
|
||||||
@@ -264,6 +273,8 @@ class AudioService extends ChangeNotifier {
|
|||||||
final song = _queue[_currentIndex];
|
final song = _queue[_currentIndex];
|
||||||
_currentSong = song;
|
_currentSong = song;
|
||||||
|
|
||||||
|
_handler?.syncState(song);
|
||||||
|
|
||||||
_startListening();
|
_startListening();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -547,6 +558,8 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stopPlay() {
|
void stopPlay() {
|
||||||
|
// 停止底层播放器
|
||||||
|
PlaybackService().stop(); // 新增
|
||||||
_currentSong = null;
|
_currentSong = null;
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
positionNotifier.value = Duration.zero;
|
positionNotifier.value = Duration.zero;
|
||||||
@@ -554,7 +567,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
bufferedNotifier.value = Duration.zero;
|
bufferedNotifier.value = Duration.zero;
|
||||||
_stopListening();
|
_stopListening();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
// 停止时也保存一次
|
|
||||||
savePlaybackState();
|
savePlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,6 +833,8 @@ class AudioService extends ChangeNotifier {
|
|||||||
_currentSong = song;
|
_currentSong = song;
|
||||||
_onSongChanged?.call(song);
|
_onSongChanged?.call(song);
|
||||||
|
|
||||||
|
_handler?.syncState(song); // ⭐ 添加这一行
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
|
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
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 '../utils/navigation.dart'; // ⭐ 导入 navigatorKey
|
||||||
|
|
||||||
class GlobalMiniPlayer extends StatelessWidget {
|
class GlobalMiniPlayer extends StatelessWidget {
|
||||||
const GlobalMiniPlayer({super.key});
|
const GlobalMiniPlayer({super.key});
|
||||||
@@ -45,11 +46,9 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// ⭐ 使用 Navigator.of(context) 替代 navigatorKey
|
// ⭐ 使用 navigatorKey 直接跳转
|
||||||
Navigator.of(context, rootNavigator: true)
|
navigatorKey.currentState?.pushNamed('/player');
|
||||||
.pushNamed('/player');
|
|
||||||
},
|
},
|
||||||
// ⭐ 替换弃用的 withOpacity 为 withValues
|
|
||||||
highlightColor: Colors.white.withValues(alpha: 0.05),
|
highlightColor: Colors.white.withValues(alpha: 0.05),
|
||||||
splashColor: Colors.white.withValues(alpha: 0.1),
|
splashColor: Colors.white.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -124,8 +123,8 @@ class GlobalMiniPlayer extends StatelessWidget {
|
|||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// ⭐ 使用 Navigator.of(context)
|
// ⭐ 使用 navigatorKey 直接跳转
|
||||||
Navigator.of(context).pushNamed('/playlist');
|
navigatorKey.currentState?.pushNamed('/playlist');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
media_kit: 1.2.6
|
media_kit: 1.2.6
|
||||||
media_kit_libs_audio: ^1.0.7
|
media_kit_libs_audio: ^1.0.7
|
||||||
#media_kit_libs_android_audio: 1.3.8
|
|
||||||
shared_preferences: 2.2.2
|
shared_preferences: 2.2.2
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
dio: ^5.4.0 # HTTP 客户端
|
dio: ^5.4.0 # HTTP 客户端
|
||||||
|
|||||||
Reference in New Issue
Block a user