From 634ffaed8c662b8bbb5c623c8124e89a0dbad7f4 Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Mon, 5 Jan 2026 17:29:24 +0800 Subject: [PATCH] 123 --- .../view/library_item_hero_section.dart | 5 +- .../core/playback_reporter.dart | 1 - lib/features/player/core/abs_audio.dart | 3 - .../player/core/abs_audio_handler.dart | 143 -------- .../player/core/abs_audio_player.dart | 316 +++++++++--------- .../player/core/abs_audio_player_mpv.dart | 108 ------ .../core/abs_audio_player_platform.dart | 159 --------- lib/features/player/core/init.dart | 123 +++---- .../player/providers/abs_provider.dart | 268 ++++++--------- .../player/providers/abs_provider.g.dart | 82 +---- .../player/view/player_expanded_desktop.dart | 58 +++- .../player/view/player_minimized.dart | 111 +++++- .../widgets/player_player_pause_button.dart | 10 +- .../view/widgets/player_progress_bar.dart | 6 +- .../shake_detector_provider.dart | 15 +- .../shake_detector_provider.g.dart | 2 +- .../skip_start_end/core/skip_start_end.dart | 100 +++--- .../providers/skip_start_end_provider.dart | 60 ++-- .../providers/skip_start_end_provider.g.dart | 25 -- .../sleep_timer/core/sleep_timer.dart | 8 +- .../providers/sleep_timer_provider.dart | 4 +- .../providers/sleep_timer_provider.g.dart | 2 +- lib/main.dart | 7 +- lib/pages/player_page.dart | 5 + lib/theme/theme.dart | 17 + pubspec.lock | 9 + pubspec.yaml | 13 +- 27 files changed, 648 insertions(+), 1012 deletions(-) delete mode 100644 lib/features/player/core/abs_audio.dart delete mode 100644 lib/features/player/core/abs_audio_handler.dart delete mode 100644 lib/features/player/core/abs_audio_player_mpv.dart delete mode 100644 lib/features/player/core/abs_audio_player_platform.dart delete mode 100644 lib/features/skip_start_end/providers/skip_start_end_provider.g.dart diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index 38af406..595dfb9 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -139,6 +139,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final book = ref.watch(currentBookProvider); final player = ref.watch(absPlayerProvider); final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull; if (libraryItem == null) { @@ -146,13 +147,13 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { } final mediaProgress = libraryItem.userMediaProgress; - if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) { + if (mediaProgress == null && book?.libraryItemId != libraryItem.id) { return const SizedBox.shrink(); } double progress; Duration remainingTime; - if (player.book?.libraryItemId == libraryItem.id) { + if (book?.libraryItemId == libraryItem.id) { // final positionStream = useStream(player.slowPositionStream); progress = (player.positionInBook).inSeconds / libraryItem.media.asBookExpanded.duration.inSeconds; diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart index dc5b944..7ade730 100644 --- a/lib/features/playback_reporting/core/playback_reporter.dart +++ b/lib/features/playback_reporting/core/playback_reporter.dart @@ -4,7 +4,6 @@ import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/features/player/core/abs_audio_player.dart'; import 'package:vaani/shared/extensions/obfuscation.dart'; -import 'package:vaani/shared/utils/error_response.dart'; final _logger = Logger('PlaybackReporter'); diff --git a/lib/features/player/core/abs_audio.dart b/lib/features/player/core/abs_audio.dart deleted file mode 100644 index 82b0f5a..0000000 --- a/lib/features/player/core/abs_audio.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:just_audio/just_audio.dart'; - -class AudiobookPlayer extends AudioPlayer {} diff --git a/lib/features/player/core/abs_audio_handler.dart b/lib/features/player/core/abs_audio_handler.dart deleted file mode 100644 index dfb52c9..0000000 --- a/lib/features/player/core/abs_audio_handler.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:vaani/features/player/core/abs_audio_player.dart'; - -// 对接audio_service -class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { - final AbsAudioPlayer _player; - - AbsAudioHandler(AbsAudioPlayer player) : _player = player { - player.mediaItemStream.listen((item) { - mediaItem.add(item); - }); - // _player.playbackEventStream.map(_transformEvent).pipe(playbackState); - - playbackState.add( - playbackState.value.copyWith( - controls: [ - MediaControl.skipToPrevious, - // if (player.state.playing) MediaControl.pause else MediaControl.play, - // MediaControl.rewind, - // MediaControl.fastForward, - MediaControl.skipToNext, - MediaControl.stop, - ], - systemActions: { - MediaAction.play, - MediaAction.pause, - MediaAction.seek, - MediaAction.seekForward, - MediaAction.seekBackward, - }, - ), - ); - - // 1. 转发播放/暂停状态 - player.playerStateStream.listen((playerState) { - playbackState.add( - playbackState.value.copyWith( - playing: playerState.playing, - // 根据 playing 和实际情况更新 processingState - processingState: const { - AbsProcessingState.idle: AudioProcessingState.idle, - AbsProcessingState.loading: AudioProcessingState.loading, - AbsProcessingState.buffering: AudioProcessingState.buffering, - AbsProcessingState.ready: AudioProcessingState.ready, - AbsProcessingState.completed: AudioProcessingState.completed, - }[playerState.processingState] ?? - AudioProcessingState.idle, - ), - ); - }); - // 2. 转发播放位置 - player.positionStream.listen((Duration position) { - playbackState.add( - playbackState.value.copyWith( - updatePosition: position, - ), - ); - }); - // 3. 转发媒体总时长 - // player.stream.duration.listen((Duration? duration) { - // // 当有新的媒体加载时,更新 mediaItem 的 duration - // final currentItem = mediaItem.value; - // if (currentItem != null && duration != null) { - // mediaItem.add(currentItem.copyWith(duration: duration)); - // } - // }); - // player.stream.completed.listen((bool playing) { - // print('播放完成'); - // }); - } - - // 播放控制方法重写 - @override - Future play() async { - await _player.play(); - } - - @override - Future pause() async { - await _player.pause(); - } - - @override - Future skipToNext() async { - await _player.next(); - } - - @override - Future skipToPrevious() async { - await _player.previous(); - } - - @override - Future seek(Duration position) async { - await _player.seek(position); - } - - @override - Future setSpeed(double speed) async { - await _player.setSpeed(speed); - } - - Future setVolume(double volume) async { - await _player.setVolume(volume); - } - - // PlaybackState _transformEvent(PlaybackEvent event) { - // return PlaybackState( - // controls: [ - // if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious, - // MediaControl.rewind, - // if (_player.playing) MediaControl.pause else MediaControl.play, - // MediaControl.stop, - // MediaControl.fastForward, - // if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext - // ], - // systemActions: { - // if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious, - // MediaAction.rewind, - // if (!(_settingsProvider?['lockSeekingNotification'] ?? false)) - // MediaAction.seek, - // MediaAction.fastForward, - // MediaAction.stop, - // MediaAction.setSpeed, - // if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext - // }, - // androidCompactActionIndices: const [1, 2, 3], - // processingState: const { - // ProcessingState.idle: AudioProcessingState.idle, - // ProcessingState.loading: AudioProcessingState.loading, - // ProcessingState.buffering: AudioProcessingState.buffering, - // ProcessingState.ready: AudioProcessingState.ready, - // ProcessingState.completed: AudioProcessingState.completed, - // }[_player.processingState]!, - // playing: _player.playing, - // updatePosition: position, - // bufferedPosition: _player.bufferedPosition, - // speed: _player.speed, - // queueIndex: event.currentIndex, - // captioningEnabled: false, - // ); - // } -} diff --git a/lib/features/player/core/abs_audio_player.dart b/lib/features/player/core/abs_audio_player.dart index 38b6ba6..594809b 100644 --- a/lib/features/player/core/abs_audio_player.dart +++ b/lib/features/player/core/abs_audio_player.dart @@ -1,8 +1,9 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:async'; -import 'package:audio_service/audio_service.dart'; import 'package:collection/collection.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:just_audio_background/just_audio_background.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; @@ -16,20 +17,32 @@ final offset = Duration(milliseconds: 10); final _logger = Logger('AbsAudioPlayer'); -/// 音频播放器抽象类 -abstract class AbsAudioPlayer { - final _mediaItemController = BehaviorSubject.seeded(null); - final playerStateSubject = - BehaviorSubject.seeded(AbsPlayerState(false, AbsProcessingState.idle)); +class AbsAudioPlayer { + late final AudioPlayer _player; + AbsAudioPlayer(AudioPlayer player) : _player = player { + _player.positionStream.listen((position) { + final chapter = currentChapter; + if (positionInBook <= (chapter?.start ?? Duration.zero) || + positionInBook >= (chapter?.end ?? Duration.zero)) { + final chapter = book?.findChapterAtTime(positionInBook); + if (chapter != currentChapter) { + // print('当前章节时长: ${currentChapter?.duration}'); + // print('切换章节时长: ${chapter?.duration}'); + // print('当前播放音轨时长: ${_player.duration}'); + chapterStreamController.add(chapter); + } + } + }); + } + final _bookStreamController = BehaviorSubject.seeded(null); final chapterStreamController = BehaviorSubject.seeded(null); BookExpanded? get book => _bookStreamController.nvalue; AudioTrack? get currentTrack => book?.tracks[currentIndex]; BookChapter? get currentChapter => chapterStreamController.nvalue; - AbsPlayerState get playerState => playerStateSubject.value; - Stream get mediaItemStream => _mediaItemController.stream; - Stream get playerStateStream => playerStateSubject.stream; + PlayerState get playerState => _player.playerState; + Stream get playerStateStream => _player.playerStateStream; // 加载整本书 Future load( @@ -59,43 +72,77 @@ abstract class AbsAudioPlayer { .formatNotificationTitle(book); chapterStreamController .add(book.findChapterAtTime(initialPosition ?? Duration.zero)); - final item = MediaItem( - id: book.libraryItemId, - title: title, - artist: artist, - duration: currentChapter?.duration ?? book.duration, - artUri: Uri.parse( - '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token', - ), - ); - _mediaItemController.sink.add(item); - final playlist = book.tracks - .map( - (track) => ( - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), - track.duration + // final item = MediaItem( + // id: book.libraryItemId, + // title: title, + // artist: artist, + // duration: currentChapter?.duration ?? book.duration, + // artUri: Uri.parse( + // '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token', + // ), + // ); + + mediaItem(track) => MediaItem( + id: book.libraryItemId + track.index.toString(), + title: title, + artist: artist, + duration: currentChapter?.duration ?? book.duration, + artUri: Uri.parse( + '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token', ), - ) - .toList(); - await setPlayList( - playlist, - index: indexTrack, - position: positionInTrack, - start: start, - end: end, - ); + ); + List audioSources = start != null && start > Duration.zero || + end != null && end > Duration.zero + ? book.tracks + .map( + (track) => ClippingAudioSource( + child: AudioSource.uri( + _getUri( + track, + downloadedUris, + baseUrl: baseUrl, + token: token, + ), + ), + start: start, + end: end == null ? null : track.duration - end, + tag: mediaItem(track), + ), + ) + .toList() + : book.tracks + .map( + (track) => AudioSource.uri( + _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), + tag: mediaItem(track), + ), + ) + .toList(); + + await _player + .setAudioSources( + audioSources, + preload: true, + initialIndex: indexTrack, + initialPosition: positionInTrack, + ) + .catchError((error) { + _logger.shout('Error in setting audio source: $error'); + return null; + }); } - Future setPlayList( - List<(Uri, Duration)> playlist, { - int? index, - Duration? position, - Duration? start, - Duration? end, - }); - Future play(); - Future pause(); - Future playOrPause(); + Future play() async { + await _player.play(); + } + + Future pause() async { + await _player.pause(); + } + + Future playOrPause() async { + _player.playing ? await _player.pause() : await _player.play(); + } // 跳到下一章 Future next() async { @@ -126,7 +173,19 @@ abstract class AbsAudioPlayer { } } - Future seek(Duration position, {int? index}); + Future seek(Duration position, {int? index}) async { + await _player.seek(_addClippingStart(_player.position, add: false), + index: index); + } + + Future setSpeed(double speed) async { + await _player.setSpeed(speed); + } + + Future setVolume(double volume) async { + await _player.setVolume(volume); + } + Future seekInBook(Duration position) async { if (book == null) return; // 找到目标位置所在音轨和音轨内的位置 @@ -140,8 +199,6 @@ abstract class AbsAudioPlayer { await seek(positionInTrack, index: index); } - Future setSpeed(double speed); - Future setVolume(double volume); Future switchChapter(int chapterId) async { if (book == null) return; @@ -153,15 +210,18 @@ abstract class AbsAudioPlayer { } bool get playing => playerState.playing; - Stream get playingStream; + Stream get playingStream => _player.playingStream; Stream get bookStream => _bookStreamController.stream; Stream get chapterStream => chapterStreamController.stream; - int get currentIndex; - double get speed; + int get currentIndex => _player.currentIndex ?? 0; + double get speed => _player.speed; - Duration get position; - Stream get positionStream; + Duration get position => _addClippingStart(_player.position); + Stream get positionStream => + _player.positionStream.where((_) => _player.playing).map((position) { + return _addClippingStart(position); + }); Duration get positionInChapter => getPositionInChapter(position); Duration getPositionInChapter(position) { @@ -183,8 +243,8 @@ abstract class AbsAudioPlayer { return positionInBook; }); - Duration get bufferedPosition; - Stream get bufferedPositionStream; + Duration get bufferedPosition => _player.bufferedPosition; + Stream get bufferedPositionStream => _player.bufferedPositionStream; Duration get bufferedPositionInBook => bufferedPosition + (book?.tracks[currentIndex].startOffset ?? Duration.zero); @@ -193,70 +253,26 @@ abstract class AbsAudioPlayer { return bufferedPositionInBook; }); + Duration _addClippingStart(Duration position, {bool add = true}) { + if (_player.sequenceState.currentSource != null && + _player.sequenceState.currentSource is ClippingAudioSource) { + final currentSource = + _player.sequenceState.currentSource as ClippingAudioSource; + if (currentSource.start != null) { + return add + ? position + currentSource.start! + : position - currentSource.start!; + } + } + return position; + } + dispose() { - _mediaItemController.close(); - playerStateSubject.close(); _bookStreamController.close(); chapterStreamController.close(); } } -/// Enumerates the different processing states of a player. -enum AbsProcessingState { - /// The player has not loaded an [AudioSource]. - idle, - - /// The player is loading an [AudioSource]. - loading, - - /// The player is buffering audio and unable to play. - buffering, - - /// The player is has enough audio buffered and is able to play. - ready, - - /// The player has reached the end of the audio. - completed, -} - -/// Encapsulates the playing and processing states. These two states vary -/// orthogonally, and so if [processingState] is [ProcessingState.buffering], -/// you can check [playing] to determine whether the buffering occurred while -/// the player was playing or while the player was paused. -class AbsPlayerState { - /// Whether the player will play when [processingState] is - /// [ProcessingState.ready]. - final bool playing; - - /// The current processing state of the player. - final AbsProcessingState processingState; - - AbsPlayerState(this.playing, this.processingState); - - @override - String toString() => 'playing=$playing,processingState=$processingState'; - - @override - int get hashCode => Object.hash(playing, processingState); - - @override - bool operator ==(Object other) => - other.runtimeType == runtimeType && - other is PlayerState && - other.playing == playing && - other.processingState == processingState; - - AbsPlayerState copyWith({ - bool? playing, - AbsProcessingState? processingState, - }) { - return AbsPlayerState( - playing ?? this.playing, - processingState ?? this.processingState, - ); - } -} - Uri _getUri( AudioTrack track, List? downloadedUris, { @@ -280,6 +296,38 @@ extension _ValueStreamExtension on ValueStream { T? get nvalue => hasValue ? value : null; } +extension BookExpandedExtension on BookExpanded { + BookChapter findChapterAtTime(Duration position) { + return chapters.firstWhere( + (element) { + return element.start <= position && element.end >= position + offset; + }, + orElse: () => chapters.first, + ); + } + + AudioTrack findTrackAtTime(Duration position) { + return tracks.firstWhere( + (element) { + return element.startOffset <= position && + element.startOffset + element.duration >= position + offset; + }, + orElse: () => tracks.first, + ); + } + + int findTrackIndexAtTime(Duration position) { + return tracks.indexWhere((element) { + return element.startOffset <= position && + element.startOffset + element.duration >= position + offset; + }); + } + + Duration getTrackStartOffset(int index) { + return tracks[index].startOffset; + } +} + extension FormatNotificationTitle on String { String formatNotificationTitle(BookExpanded book) { return replaceAllMapped( @@ -318,49 +366,3 @@ extension NotificationTitleUtils on NotificationTitleType { } } } - -extension BookExpandedExtension on BookExpanded { - BookChapter findChapterAtTime(Duration position) { - return chapters.firstWhere( - (element) { - return element.start <= position && element.end >= position + offset; - }, - orElse: () => chapters.first, - ); - } - - AudioTrack findTrackAtTime(Duration position) { - return tracks.firstWhere( - (element) { - return element.startOffset <= position && - element.startOffset + element.duration >= position + offset; - }, - orElse: () => tracks.first, - ); - } - - int findTrackIndexAtTime(Duration position) { - return tracks.indexWhere((element) { - return element.startOffset <= position && - element.startOffset + element.duration >= position + offset; - }); - } - - Duration getTrackStartOffset(int index) { - return tracks[index].startOffset; - } -} - -class AudioMetadata { - final String album; - final String title; - final String artist; - final String artwork; - - AudioMetadata({ - required this.album, - required this.title, - required this.artist, - required this.artwork, - }); -} diff --git a/lib/features/player/core/abs_audio_player_mpv.dart b/lib/features/player/core/abs_audio_player_mpv.dart deleted file mode 100644 index 48d1e64..0000000 --- a/lib/features/player/core/abs_audio_player_mpv.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:async'; - -import 'package:media_kit/media_kit.dart' hide PlayerState; -import 'package:vaani/features/player/core/abs_audio_player.dart'; - -/// 音频播放器 mpv全平台 (media_kit) -class AbsMpvAudioPlayer extends AbsAudioPlayer { - late Player player; - AbsMpvAudioPlayer() { - MediaKit.ensureInitialized(); - player = Player(); - player.stream.playing.listen((playing) { - final state = playerState; - playerStateSubject.add( - state.copyWith( - playing: playing, - processingState: playing - ? state.processingState == AbsProcessingState.idle - ? AbsProcessingState.ready - : state.processingState - : player.state.buffering - ? AbsProcessingState.buffering - : player.state.completed - ? AbsProcessingState.completed - : AbsProcessingState.ready, - ), - ); - }); - } - @override - Duration get bufferedPosition => player.state.buffer; - - @override - Stream get bufferedPositionStream => player.stream.buffer; - - @override - int get currentIndex => player.state.playlist.index; - - @override - Future pause() async { - await player.pause(); - } - - @override - Future play() async { - await player.play(); - } - - @override - Future playOrPause() async { - await player.playOrPause(); - } - - @override - Stream get playingStream => player.stream.playing; - - @override - Duration get position => player.state.position; - - @override - Stream get positionStream => player.stream.position; - - @override - Future seek(Duration position, {int? index}) async { - if (index != null) { - final playing = this.playing; - await player.jump(index); - if (!playing) await player.pause(); - } - await player.seek(position); - } - - @override - Future setPlayList( - List<(Uri, Duration)> playlist, { - int? index, - Duration? position, - Duration? start, - Duration? end, - }) async { - await player.open( - Playlist( - playlist.map((uri) => Media(uri.$1.toString())).toList(), - index: index ?? 0, - ), - play: false, - ); - // 等待open方法加载完成 - // ignore: unnecessary_null_comparison - await player.stream.duration.firstWhere((d) => d != null); - if (position != null) { - await player.seek(position); - } - } - - @override - Future setSpeed(double speed) async { - await player.setRate(speed); - } - - @override - Future setVolume(double volume) async { - await player.setVolume(volume * 100); - } - - @override - double get speed => player.state.rate; -} diff --git a/lib/features/player/core/abs_audio_player_platform.dart b/lib/features/player/core/abs_audio_player_platform.dart deleted file mode 100644 index f512548..0000000 --- a/lib/features/player/core/abs_audio_player_platform.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:just_audio/just_audio.dart'; -import 'package:just_audio_media_kit/just_audio_media_kit.dart'; -import 'package:logging/logging.dart'; -import 'package:vaani/features/player/core/abs_audio_player.dart'; - -final _logger = Logger('AbsPlatformAudioPlayer'); - -/// 音频播放器 平台ios,macos,android (just_audio) -class AbsPlatformAudioPlayer extends AbsAudioPlayer { - late final AudioPlayer _player; - AbsPlatformAudioPlayer() { - // 跳转到播放列表指定条目指定位置 - // prefetch-playlist=yes - JustAudioMediaKit.prefetchPlaylist = true; - // merge-files=yes - // cache=yes - // cache-pause-wait=60 - - JustAudioMediaKit.ensureInitialized(); - _player = AudioPlayer(); - _player.playerStateStream.listen((state) { - playerStateSubject.add( - playerState.copyWith( - playing: state.playing, - processingState: { - ProcessingState.idle: AbsProcessingState.idle, - ProcessingState.buffering: AbsProcessingState.buffering, - ProcessingState.completed: AbsProcessingState.completed, - ProcessingState.loading: AbsProcessingState.loading, - ProcessingState.ready: AbsProcessingState.ready, - }[state.processingState], - ), - ); - }); - positionStream.listen((position) { - final chapter = currentChapter; - if (positionInBook <= (chapter?.start ?? Duration.zero) || - positionInBook >= (chapter?.end ?? Duration.zero)) { - final chapter = book?.findChapterAtTime(positionInBook); - if (chapter != currentChapter) { - // print('当前章节时长: ${currentChapter?.duration}'); - // print('切换章节时长: ${chapter?.duration}'); - // print('当前播放音轨时长: ${_player.duration}'); - chapterStreamController.add(chapter); - } - } - }); - } - @override - Duration get bufferedPosition => _player.bufferedPosition; - - @override - Stream get bufferedPositionStream => _player.bufferedPositionStream - .where( - (_) => _player.playerState.processingState == ProcessingState.buffering, - ) - .asBroadcastStream(); - - @override - int get currentIndex => _player.currentIndex ?? 0; - - @override - Future pause() async { - await _player.pause(); - } - - @override - Future play() async { - await _player.play(); - } - - @override - Future playOrPause() async { - _player.playing ? await _player.pause() : await _player.play(); - } - - @override - Stream get playingStream => _player.playingStream; - - @override - Duration get position => _addClippingStart(_player.position); - - Duration _addClippingStart(Duration position, {bool add = true}) { - if (_player.sequenceState.currentSource != null && - _player.sequenceState.currentSource is ClippingAudioSource) { - final currentSource = - _player.sequenceState.currentSource as ClippingAudioSource; - if (currentSource.start != null) { - return add - ? position + currentSource.start! - : position - currentSource.start!; - } - } - return position; - } - - @override - Stream get positionStream => - _player.positionStream.where((_) => _player.playing).map((position) { - return _addClippingStart(position); - }); - - @override - Future seek(Duration position, {int? index}) async { - await _player.seek(_addClippingStart(position, add: false), index: index); - } - - @override - Future setPlayList( - List<(Uri, Duration)> playlist, { - int? index, - Duration? position, - Duration? start, - Duration? end, - }) async { - List audioSources = start != null && start > Duration.zero || - end != null && end > Duration.zero - ? playlist - .map( - (item) => ClippingAudioSource( - child: AudioSource.uri(item.$1), - start: start, - end: end == null ? null : item.$2 - end, - ), - ) - .toList() - : playlist.map((item) => AudioSource.uri(item.$1)).toList(); - await _player - .setAudioSources( - audioSources, - preload: true, - initialIndex: index, - initialPosition: position, - ) - .catchError((error) { - _logger.shout('Error in setting audio source: $error'); - return null; - }); - } - - @override - Future setSpeed(double speed) async { - await _player.setSpeed(speed); - } - - @override - Future setVolume(double volume) async { - await _player.setVolume(volume); - } - - @override - double get speed => _player.speed; - - @override - void dispose() { - super.dispose(); - _player.dispose(); - } -} diff --git a/lib/features/player/core/init.dart b/lib/features/player/core/init.dart index a2e3ec4..dde81ca 100644 --- a/lib/features/player/core/init.dart +++ b/lib/features/player/core/init.dart @@ -1,62 +1,69 @@ -// import 'package:audio_service/audio_service.dart'; -// import 'package:audio_session/audio_session.dart'; -// import 'package:just_audio_background/just_audio_background.dart' -// show JustAudioBackground, NotificationConfig; -// import 'package:just_audio_media_kit/just_audio_media_kit.dart' -// show JustAudioMediaKit; -// import 'package:vaani/features/settings/app_settings_provider.dart'; -// import 'package:vaani/features/settings/models/app_settings.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:just_audio_background/just_audio_background.dart' + show JustAudioBackground, NotificationConfig; +import 'package:just_audio_media_kit/just_audio_media_kit.dart' + show JustAudioMediaKit; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; -// Future configurePlayer() async { -// // for playing audio on windows, linux -// JustAudioMediaKit.ensureInitialized(); +Future configurePlayer() async { + // for playing audio on windows, linux + JustAudioMediaKit.ensureInitialized(); -// // for configuring how this app will interact with other audio apps -// final session = await AudioSession.instance; -// await session.configure(const AudioSessionConfiguration.speech()); + // 跳转到播放列表指定条目指定位置 + // prefetch-playlist=yes + // JustAudioMediaKit.prefetchPlaylist = true; + // merge-files=yes + // cache=yes + // cache-pause-wait=60 -// final appSettings = loadOrCreateAppSettings(); + // for configuring how this app will interact with other audio apps + final session = await AudioSession.instance; + await session.configure(const AudioSessionConfiguration.speech()); -// // for playing audio in the background -// await JustAudioBackground.init( -// androidNotificationChannelId: 'dr.blank.vaani.channel.audio', -// androidNotificationChannelName: 'Audio playback', -// androidNotificationOngoing: false, -// androidStopForegroundOnPause: false, -// androidNotificationChannelDescription: 'Audio playback in the background', -// androidNotificationIcon: 'drawable/ic_stat_logo', -// rewindInterval: appSettings.notificationSettings.rewindInterval, -// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, -// androidShowNotificationBadge: false, -// notificationConfigBuilder: (state) { -// final controls = [ -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.skipToPreviousChapter) && -// state.hasPrevious) -// MediaControl.skipToPrevious, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.rewind)) -// MediaControl.rewind, -// if (state.playing) MediaControl.pause else MediaControl.play, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.fastForward)) -// MediaControl.fastForward, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.skipToNextChapter) && -// state.hasNext) -// MediaControl.skipToNext, -// if (appSettings.notificationSettings.mediaControls -// .contains(NotificationMediaControl.stop)) -// MediaControl.stop, -// ]; -// return NotificationConfig( -// controls: controls, -// systemActions: const { -// MediaAction.seek, -// MediaAction.seekForward, -// MediaAction.seekBackward, -// }, -// ); -// }, -// ); -// } + final appSettings = loadOrCreateAppSettings(); + + // for playing audio in the background + await JustAudioBackground.init( + androidNotificationChannelId: 'dr.blank.vaani.channel.audio', + androidNotificationChannelName: 'Audio playback', + androidNotificationOngoing: false, + androidStopForegroundOnPause: false, + androidNotificationChannelDescription: 'Audio playback in the background', + androidNotificationIcon: 'drawable/ic_stat_logo', + rewindInterval: appSettings.notificationSettings.rewindInterval, + fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, + androidShowNotificationBadge: false, + notificationConfigBuilder: (state) { + final controls = [ + if (appSettings.notificationSettings.mediaControls + .contains(NotificationMediaControl.skipToPreviousChapter) && + state.hasPrevious) + MediaControl.skipToPrevious, + if (appSettings.notificationSettings.mediaControls + .contains(NotificationMediaControl.rewind)) + MediaControl.rewind, + if (state.playing) MediaControl.pause else MediaControl.play, + if (appSettings.notificationSettings.mediaControls + .contains(NotificationMediaControl.fastForward)) + MediaControl.fastForward, + if (appSettings.notificationSettings.mediaControls + .contains(NotificationMediaControl.skipToNextChapter) && + state.hasNext) + MediaControl.skipToNext, + if (appSettings.notificationSettings.mediaControls + .contains(NotificationMediaControl.stop)) + MediaControl.stop, + ]; + return NotificationConfig( + controls: controls, + systemActions: const { + MediaAction.seek, + MediaAction.seekForward, + MediaAction.seekBackward, + }, + ); + }, + ); +} diff --git a/lib/features/player/providers/abs_provider.dart b/lib/features/player/providers/abs_provider.dart index 42e8272..0c81aeb 100644 --- a/lib/features/player/providers/abs_provider.dart +++ b/lib/features/player/providers/abs_provider.dart @@ -1,9 +1,6 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:audio_session/audio_session.dart'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:logging/logging.dart'; +import 'package:just_audio/just_audio.dart' as audio; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as api; import 'package:vaani/api/api_provider.dart'; @@ -12,55 +9,52 @@ import 'package:vaani/db/available_boxes.dart'; import 'package:vaani/db/cache/cache_key.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; -import 'package:vaani/features/player/core/abs_audio_handler.dart'; -import 'package:vaani/features/player/core/abs_audio_player.dart' as core; -import 'package:vaani/features/player/core/abs_audio_player_platform.dart'; +import 'package:vaani/features/player/core/abs_audio_player.dart' + show AbsAudioPlayer; import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/box.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; part 'abs_provider.g.dart'; -final _logger = Logger('AbsPlayerProvider'); - /// 音频播放器 配置 -@Riverpod(keepAlive: true) -Future configurePlayer(Ref ref) async { - final player = ref.read(absPlayerProvider); - // for playing audio on windows, linux +// @Riverpod(keepAlive: true) +// Future configurePlayer(Ref ref) async { +// final player = ref.read(absPlayerProvider); +// // for playing audio on windows, linux - // for configuring how this app will interact with other audio apps - final session = await AudioSession.instance; - await session.configure(const AudioSessionConfiguration.speech()); +// // for configuring how this app will interact with other audio apps +// final session = await AudioSession.instance; +// await session.configure(const AudioSessionConfiguration.speech()); - final audioService = await AudioService.init( - builder: () => AbsAudioHandler(player), - config: const AudioServiceConfig( - androidNotificationChannelId: 'dr.blank.vaani.channel.audio', - androidNotificationChannelName: 'ABSPlayback', - androidNotificationChannelDescription: - 'Needed to control audio from lock screen', - androidNotificationOngoing: false, - androidStopForegroundOnPause: false, - androidNotificationIcon: 'drawable/ic_stat_logo', - preloadArtwork: true, - // fastForwardInterval: Duration(seconds: 20), - // rewindInterval: Duration(seconds: 20), - ), - ); +// final audioService = await AudioService.init( +// builder: () => AbsAudioHandler(player), +// config: const AudioServiceConfig( +// androidNotificationChannelId: 'dr.blank.vaani.channel.audio', +// androidNotificationChannelName: 'ABSPlayback', +// androidNotificationChannelDescription: +// 'Needed to control audio from lock screen', +// androidNotificationOngoing: false, +// androidStopForegroundOnPause: false, +// androidNotificationIcon: 'drawable/ic_stat_logo', +// preloadArtwork: true, +// // fastForwardInterval: Duration(seconds: 20), +// // rewindInterval: Duration(seconds: 20), +// ), +// ); - _logger.finer('created simple player'); - return audioService; -} +// _logger.finer('created simple player'); +// return audioService; +// } // just_audio 播放器 -@Riverpod(keepAlive: true) -core.AbsAudioPlayer audioPlayer(Ref ref) { - final player = AbsPlatformAudioPlayer(); - // final player = AbsMpvAudioPlayer(); - ref.onDispose(player.dispose); - return player; -} +// @Riverpod(keepAlive: true) +// core.AbsAudioPlayer audioPlayer(Ref ref) { +// final player = AbsPlatformAudioPlayer(); +// // final player = AbsMpvAudioPlayer(); +// ref.onDispose(player.dispose); +// return player; +// } // 播放器激活状态 @riverpod @@ -69,129 +63,20 @@ bool playerActive(Ref ref) { } @Riverpod(keepAlive: true) -AudioPlayer simpleAudioPlayer(Ref ref) { - final player = AudioPlayer(); +audio.AudioPlayer simpleAudioPlayer(Ref ref) { + final player = audio.AudioPlayer(); ref.onDispose(player.dispose); return player; } -@Riverpod(keepAlive: true) -class AbsAudioPlayer extends _$AbsAudioPlayer { - @override - AudioPlayer build() { - final audioPlayer = ref.watch(simpleAudioPlayerProvider); - return audioPlayer; - } +final offset = Duration(milliseconds: 10); - Future load( - api.BookExpanded book, { - Duration? initialPosition, - bool play = true, - }) async { - final currentTrack = book.findTrackAtTime(initialPosition ?? Duration.zero); - final indexTrack = book.tracks.indexOf(currentTrack); - final positionInTrack = initialPosition != null - ? initialPosition - currentTrack.startOffset - : null; - final api = ref.read(authenticatedApiProvider); - - final downloadManager = ref.read(simpleDownloadManagerProvider); - print(downloadManager.basePath); - - final libItem = - await ref.read(libraryItemProvider(book.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); - - final bookSettings = ref.read(bookSettingsProvider(book.libraryItemId)); - var bookPlayerSettings = bookSettings.playerSettings; - final start = bookSettings.playerSettings.skipChapterStart; - final end = bookSettings.playerSettings.skipChapterEnd; - final appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - final configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; - List audioSources = - start > Duration.zero || end > Duration.zero - ? book.tracks - .map( - (track) => ClippingAudioSource( - child: AudioSource.uri( - _getUri( - track, - downloadedUris, - baseUrl: api.baseUrl, - token: api.token!, - ), - ), - start: start, - end: end > Duration.zero ? null : track.duration - end, - ), - ) - .toList() - : book.tracks - .map( - (track) => AudioSource.uri( - _getUri( - track, - downloadedUris, - baseUrl: api.baseUrl, - token: api.token!, - ), - ), - ) - .toList(); - await state - .setAudioSources( - audioSources, - preload: true, - initialIndex: indexTrack, - initialPosition: positionInTrack, - ) - .catchError((error) { - _logger.shout('Error in setting audio source: $error'); - return null; - }); - // set the volume - await state.setVolume( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultVolume ?? - appPlayerSettings.preferredDefaultVolume - : appPlayerSettings.preferredDefaultVolume, - ); - // set the speed - await state.setSpeed( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed - : appPlayerSettings.preferredDefaultSpeed, - ); - if (play) await state.play(); - } - - Uri _getUri( - api.AudioTrack track, - List? downloadedUris, { - required Uri baseUrl, - required String token, - }) { - // check if the track is in the downloadedUris - final uri = downloadedUris?.firstWhereOrNull( - (element) { - return element.pathSegments.last == track.metadata?.filename; - }, - ); - - return uri ?? - Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); - } -} - -/// 音频播放器 riverpod状态 @Riverpod(keepAlive: true) class AbsPlayer extends _$AbsPlayer { @override - core.AbsAudioPlayer build() { - final audioPlayer = ref.watch(audioPlayerProvider); - return audioPlayer; + AbsAudioPlayer build() { + final audioPlayer = ref.watch(simpleAudioPlayerProvider); + return AbsAudioPlayer(audioPlayer); } Future load( @@ -246,10 +131,71 @@ class AbsPlayer extends _$AbsPlayer { } } +/// 音频播放器 riverpod状态 +// @Riverpod(keepAlive: true) +// class AbsPlayer extends _$AbsPlayer { +// @override +// core.AbsAudioPlayer build() { +// final audioPlayer = ref.watch(audioPlayerProvider); +// return audioPlayer; +// } + +// Future load( +// api.BookExpanded book, { +// Duration? initialPosition, +// bool play = true, +// }) async { +// if (state.book == book || state.book?.libraryItemId == book.libraryItemId) { +// state.playOrPause(); +// return; +// } +// final api = ref.read(authenticatedApiProvider); + +// final downloadManager = ref.read(simpleDownloadManagerProvider); +// print(downloadManager.basePath); + +// final libItem = +// await ref.read(libraryItemProvider(book.libraryItemId).future); +// final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); + +// final bookSettings = ref.read(bookSettingsProvider(book.libraryItemId)); +// var bookPlayerSettings = bookSettings.playerSettings; +// var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; + +// var configurePlayerForEveryBook = +// appPlayerSettings.configurePlayerForEveryBook; + +// await state.load( +// book, +// baseUrl: api.baseUrl, +// token: api.token!, +// initialPosition: initialPosition, +// downloadedUris: downloadedUris, +// start: bookSettings.playerSettings.skipChapterStart, +// end: bookSettings.playerSettings.skipChapterEnd, +// ); +// // set the volume +// await state.setVolume( +// configurePlayerForEveryBook +// ? bookPlayerSettings.preferredDefaultVolume ?? +// appPlayerSettings.preferredDefaultVolume +// : appPlayerSettings.preferredDefaultVolume, +// ); +// // set the speed +// await state.setSpeed( +// configurePlayerForEveryBook +// ? bookPlayerSettings.preferredDefaultSpeed ?? +// appPlayerSettings.preferredDefaultSpeed +// : appPlayerSettings.preferredDefaultSpeed, +// ); +// if (play) await state.play(); +// } +// } + @riverpod class PlayerState extends _$PlayerState { @override - core.AbsPlayerState build() { + audio.PlayerState build() { final player = ref.read(absPlayerProvider); player.playerStateStream.listen((playerState) { if (playerState != state) { @@ -260,10 +206,10 @@ class PlayerState extends _$PlayerState { } bool isLoading(String itemId) { - final player = ref.read(absPlayerProvider); - return player.book?.libraryItemId == itemId && + final book = ref.read(currentBookProvider); + return book?.libraryItemId == itemId && !state.playing && - state.processingState == core.AbsProcessingState.loading; + state.processingState == audio.ProcessingState.loading; } bool isPlaying() { @@ -307,7 +253,7 @@ class CurrentBook extends _$CurrentBook { Future update(String libraryItemId, {bool play = true}) async { if (state?.libraryItemId == libraryItemId) { - ref.read(audioPlayerProvider).playOrPause(); + ref.read(absPlayerProvider).playOrPause(); return; } final book = await ref.read(libraryItemProvider(libraryItemId).future); diff --git a/lib/features/player/providers/abs_provider.g.dart b/lib/features/player/providers/abs_provider.g.dart index fbb2c90..02cc90a 100644 --- a/lib/features/player/providers/abs_provider.g.dart +++ b/lib/features/player/providers/abs_provider.g.dart @@ -6,44 +6,11 @@ part of 'abs_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$configurePlayerHash() => r'7ac63b6c3a34c56f42be55bc7a4856dabaae1583'; +String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910'; /// 音频播放器 配置 /// -/// Copied from [configurePlayer]. -@ProviderFor(configurePlayer) -final configurePlayerProvider = FutureProvider.internal( - configurePlayer, - name: r'configurePlayerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$configurePlayerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ConfigurePlayerRef = FutureProviderRef; -String _$audioPlayerHash() => r'156f85effafdcd287db88e455e8f4f4d33c41a0e'; - -/// See also [audioPlayer]. -@ProviderFor(audioPlayer) -final audioPlayerProvider = Provider.internal( - audioPlayer, - name: r'audioPlayerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$audioPlayerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AudioPlayerRef = ProviderRef; -String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910'; - -/// See also [playerActive]. +/// Copied from [playerActive]. @ProviderFor(playerActive) final playerActiveProvider = AutoDisposeProvider.internal( playerActive, @@ -57,11 +24,11 @@ final playerActiveProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef PlayerActiveRef = AutoDisposeProviderRef; -String _$simpleAudioPlayerHash() => r'4da667e3b7047003edd594f8a76700afb963aceb'; +String _$simpleAudioPlayerHash() => r'99d84a750cf605ad036603320925f0ba7253930b'; /// See also [simpleAudioPlayer]. @ProviderFor(simpleAudioPlayer) -final simpleAudioPlayerProvider = Provider.internal( +final simpleAudioPlayerProvider = Provider.internal( simpleAudioPlayer, name: r'simpleAudioPlayerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -73,7 +40,7 @@ final simpleAudioPlayerProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef SimpleAudioPlayerRef = ProviderRef; +typedef SimpleAudioPlayerRef = ProviderRef; String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9'; /// Copied from Dart SDK @@ -242,30 +209,11 @@ final positionChapterProvider = AutoDisposeStreamProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef PositionChapterRef = AutoDisposeStreamProviderRef; -String _$absAudioPlayerHash() => r'f595b5033eed9f4a4aa07c297c4a176955e6aab1'; +String _$absPlayerHash() => r'370f576d3d3a2196d1a93f2046005c1a3298d994'; -/// See also [AbsAudioPlayer]. -@ProviderFor(AbsAudioPlayer) -final absAudioPlayerProvider = - NotifierProvider.internal( - AbsAudioPlayer.new, - name: r'absAudioPlayerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$absAudioPlayerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$AbsAudioPlayer = Notifier; -String _$absPlayerHash() => r'e682fea03793a0370cb143602980d5c1e37396c7'; - -/// 音频播放器 riverpod状态 -/// -/// Copied from [AbsPlayer]. +/// See also [AbsPlayer]. @ProviderFor(AbsPlayer) -final absPlayerProvider = - NotifierProvider.internal( +final absPlayerProvider = NotifierProvider.internal( AbsPlayer.new, name: r'absPlayerProvider', debugGetCreateSourceHash: @@ -274,13 +222,15 @@ final absPlayerProvider = allTransitiveDependencies: null, ); -typedef _$AbsPlayer = Notifier; -String _$playerStateHash() => r'f195d2d13bcee0f91b862e669ab3549667d8dd2d'; +typedef _$AbsPlayer = Notifier; +String _$playerStateHash() => r'eb79bd816714f721da1c4226d4447de5dc55fc5c'; -/// See also [PlayerState]. +/// 音频播放器 riverpod状态 +/// +/// Copied from [PlayerState]. @ProviderFor(PlayerState) final playerStateProvider = - AutoDisposeNotifierProvider.internal( + AutoDisposeNotifierProvider.internal( PlayerState.new, name: r'playerStateProvider', debugGetCreateSourceHash: @@ -289,8 +239,8 @@ final playerStateProvider = allTransitiveDependencies: null, ); -typedef _$PlayerState = AutoDisposeNotifier; -String _$currentBookHash() => r'714d7701508b6186598e13bc38c57c3fe644ae90'; +typedef _$PlayerState = AutoDisposeNotifier; +String _$currentBookHash() => r'85de9041d356e214761b65bd1b7b74321d5a9221'; /// See also [CurrentBook]. @ProviderFor(CurrentBook) diff --git a/lib/features/player/view/player_expanded_desktop.dart b/lib/features/player/view/player_expanded_desktop.dart index 597dbc8..74f2591 100644 --- a/lib/features/player/view/player_expanded_desktop.dart +++ b/lib/features/player/view/player_expanded_desktop.dart @@ -62,17 +62,17 @@ class PlayerExpandedDesktop extends HookConsumerWidget { // add a shadow to the image elevation hovering effect child: PlayerExpandedImage(imageSize), ), - _buildControls(imageSize), - SizedBox( - width: imageSize, - child: Padding( - padding: EdgeInsets.only( - left: AppElementSizes.paddingRegular, - right: AppElementSizes.paddingRegular, - ), - child: const AudiobookChapterProgressBar(), - ), - ), + // _buildControls(imageSize), + // SizedBox( + // width: imageSize, + // child: Padding( + // padding: EdgeInsets.only( + // left: AppElementSizes.paddingRegular, + // right: AppElementSizes.paddingRegular, + // ), + // child: const AudiobookChapterProgressBar(), + // ), + // ), _buildSettings(imageSize), ], ), @@ -113,7 +113,37 @@ class PlayerExpandedDesktop extends HookConsumerWidget { ), ), ), - Hero(tag: 'player_hero', child: const PlayerMinimizedControls()), + SizedBox( + height: playerMinimizedHeight, + child: _buildBottom(), + ), + ], + ); + } + + Widget _buildBottom() { + return Row( + children: [ + SizedBox( + width: 180, + child: Row( + children: [ + const AudiobookPlayerSeekChapterButton(isForward: false), + // play/pause button + const AudiobookPlayerPlayPauseButton(), + const AudiobookPlayerSeekChapterButton(isForward: true), + ], + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: AppElementSizes.paddingRegular, + right: AppElementSizes.paddingRegular, + ), + child: const AudiobookChapterProgressBar(), + ), + ), ], ); } @@ -128,7 +158,7 @@ class PlayerExpandedDesktop extends HookConsumerWidget { const AudiobookPlayerSeekChapterButton(isForward: false), // buttonSkipBackwards const AudiobookPlayerSeekButton(isForward: false), - AudiobookPlayerPlayPauseButton(), + const AudiobookPlayerPlayPauseButton(), // // buttonSkipForwards const AudiobookPlayerSeekButton(isForward: true), // // next chapter @@ -144,6 +174,8 @@ class PlayerExpandedDesktop extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + const AudiobookPlayerSeekButton(isForward: false), + const AudiobookPlayerSeekButton(isForward: true), // speed control const PlayerSpeedAdjustButton(), const Spacer(), diff --git a/lib/features/player/view/player_minimized.dart b/lib/features/player/view/player_minimized.dart index 1b74866..0636cc9 100644 --- a/lib/features/player/view/player_minimized.dart +++ b/lib/features/player/view/player_minimized.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/abs_provider.dart'; +import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/shared/extensions/chapter.dart'; @@ -22,6 +23,9 @@ class PlayerMinimized extends HookConsumerWidget { if (currentBook == null) { return SizedBox.shrink(); } + final size = MediaQuery.of(context).size; + // 竖屏 + final isVertical = size.height > size.width; return GestureDetector( child: Container( height: playerMinimizedHeight, @@ -29,8 +33,10 @@ class PlayerMinimized extends HookConsumerWidget { child: Stack( alignment: Alignment.topCenter, children: [ - Hero(tag: 'player_hero', child: const PlayerMinimizedControls()), - PlayerMinimizedProgress(), + isVertical + ? const PlayerMinimizedControls() + : const PlayerMinimizedControlsDesktop(), + const PlayerMinimizedProgress(), ], ), ), @@ -130,10 +136,7 @@ class PlayerMinimizedControls extends HookConsumerWidget { ), // play/pause button - Padding( - padding: const EdgeInsets.only(right: 8), - child: AudiobookPlayerPlayPauseButton(), - ), + const AudiobookPlayerPlayPauseButton(), ], ), ); @@ -162,3 +165,99 @@ class PlayerMinimizedProgress extends HookConsumerWidget { ); } } + +class PlayerMinimizedControlsDesktop extends HookConsumerWidget { + const PlayerMinimizedControlsDesktop({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentBook = ref.watch(currentBookProvider); + final currentChapter = ref.watch(currentChapterProvider); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (GoRouterState.of(context).topRoute?.name != Routes.player.name) { + context.pushNamed(Routes.player.name); + } else { + context.pop(); + } + }, + child: Row( + children: [ + SizedBox( + width: 180, + child: Row( + children: [ + const AudiobookPlayerSeekChapterButton(isForward: false), + // play/pause button + const AudiobookPlayerPlayPauseButton(), + const AudiobookPlayerSeekChapterButton(isForward: true), + ], + ), + ), + + // image + Padding( + padding: EdgeInsets.all(AppElementSizes.paddingSmall), + child: GestureDetector( + onTap: () { + // navigate to item page + if (currentBook != null) { + context.pushNamed( + Routes.libraryItem.name, + pathParameters: { + Routes.libraryItem.pathParamName!: + currentBook.libraryItemId, + }, + ); + } + }, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: playerMinimizedHeight, + ), + child: BookCoverWidget(), + ), + ), + ), + // author and title of the book + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: AppElementSizes.paddingRegular, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // AutoScrollText( + Text( + '${currentBook?.metadata.title ?? ''} - ${currentChapter?.title ?? ''}', + maxLines: 1, overflow: TextOverflow.ellipsis, + // velocity: + // const Velocity(pixelsPerSecond: Offset(16, 0)), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + currentBook?.metadata.asBookMetadataExpanded.authorName ?? + '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/player/view/widgets/player_player_pause_button.dart b/lib/features/player/view/widgets/player_player_pause_button.dart index 629ae8f..b814e53 100644 --- a/lib/features/player/view/widgets/player_player_pause_button.dart +++ b/lib/features/player/view/widgets/player_player_pause_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:vaani/features/player/providers/abs_provider.dart' hide PlayerState; -import 'package:vaani/features/player/core/abs_audio_player.dart'; class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { const AudiobookPlayerPlayPauseButton({ @@ -21,12 +21,12 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { ); } - Widget _getIcon(AbsPlayerState playerState, BuildContext context) { + Widget _getIcon(PlayerState playerState, BuildContext context) { if (playerState.playing) { return Icon(size: iconSize, Icons.pause); } else { switch (playerState.processingState) { - case AbsProcessingState.loading || AbsProcessingState.buffering: + case ProcessingState.loading || ProcessingState.buffering: return CircularProgressIndicator(); default: return Icon(size: iconSize, Icons.play_arrow); @@ -34,13 +34,13 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { } } - void _actionButtonPressed(AbsPlayerState playerState, WidgetRef ref) async { + void _actionButtonPressed(PlayerState playerState, WidgetRef ref) async { final player = ref.read(absPlayerProvider); if (playerState.playing) { await player.pause(); } else { switch (playerState.processingState) { - case AbsProcessingState.completed: + case ProcessingState.completed: await player.seekInBook(const Duration(seconds: 0)); await player.play(); default: diff --git a/lib/features/player/view/widgets/player_progress_bar.dart b/lib/features/player/view/widgets/player_progress_bar.dart index ad4ad7c..2aab8e6 100644 --- a/lib/features/player/view/widgets/player_progress_bar.dart +++ b/lib/features/player/view/widgets/player_progress_bar.dart @@ -12,6 +12,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final book = ref.watch(currentBookProvider); final player = ref.watch(absPlayerProvider); final currentChapter = ref.watch(currentChapterProvider); final position = useStream( @@ -36,7 +37,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { final progress = currentChapterProgress ?? position.data ?? const Duration(seconds: 0); final total = currentChapter == null - ? player.book?.duration ?? const Duration(seconds: 0) + ? book?.duration ?? const Duration(seconds: 0) : currentChapter.end - currentChapter.start; return ProgressBar( progress: progress, @@ -65,6 +66,7 @@ class AudiobookProgressBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final book = ref.watch(currentBookProvider); final player = ref.read(absPlayerProvider); final position = useStream( player.positionInBookStream, @@ -75,7 +77,7 @@ class AudiobookProgressBar extends HookConsumerWidget { height: AppElementSizes.barHeightLarge, child: LinearProgressIndicator( value: (position.data ?? const Duration(seconds: 0)).inSeconds / - (player.book?.duration ?? const Duration(seconds: 0)).inSeconds, + (book?.duration ?? const Duration(seconds: 0)).inSeconds, borderRadius: BorderRadiusGeometry.all(Radius.circular(10)), ), ); diff --git a/lib/features/shake_detector/shake_detector_provider.dart b/lib/features/shake_detector/shake_detector_provider.dart index 0a47458..5b8c540 100644 --- a/lib/features/shake_detector/shake_detector_provider.dart +++ b/lib/features/shake_detector/shake_detector_provider.dart @@ -9,7 +9,6 @@ import 'package:vaani/features/settings/app_settings_provider.dart' import 'package:vaani/features/settings/models/app_settings.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' show sleepTimerProvider; -import 'package:vaani/features/player/core/abs_audio_player.dart'; import 'package:vibration/vibration.dart'; import 'shake_detector.dart' as core; @@ -33,22 +32,22 @@ class ShakeDetector extends _$ShakeDetector { } // if no book is loaded, shake detection should not be enabled + final book = ref.watch(currentBookProvider); final player = ref.watch(absPlayerProvider); player.playerStateStream.listen((event) { - if (event.processingState == AbsProcessingState.idle && wasPlayerLoaded) { + if (event.processingState == ProcessingState.idle && wasPlayerLoaded) { _logger.config('Player is now not loaded, invalidating'); wasPlayerLoaded = false; ref.invalidateSelf(); } - if (event.processingState != AbsProcessingState.idle && - !wasPlayerLoaded) { + if (event.processingState != ProcessingState.idle && !wasPlayerLoaded) { _logger.config('Player is now loaded, invalidating'); wasPlayerLoaded = true; ref.invalidateSelf(); } }); - if (player.book == null) { + if (book == null) { _logger.config('No book is loaded, disabling shake detection'); wasPlayerLoaded = false; return null; @@ -89,8 +88,10 @@ class ShakeDetector extends _$ShakeDetector { ShakeAction shakeAction, { required Ref ref, }) { + final book = ref.read(currentBookProvider); + final player = ref.read(absPlayerProvider); - if (player.book == null && shakeAction.isPlaybackManagementEnabled) { + if (book == null && shakeAction.isPlaybackManagementEnabled) { _logger.warning('No book is loaded'); return false; } @@ -122,7 +123,7 @@ class ShakeDetector extends _$ShakeDetector { return true; case ShakeAction.playPause: _logger.fine('Toggling play/pause'); - player.playOrPause(); + player.playing ? player.pause() : player.play(); return true; default: return false; diff --git a/lib/features/shake_detector/shake_detector_provider.g.dart b/lib/features/shake_detector/shake_detector_provider.g.dart index 85d2fbf..d9cd6a6 100644 --- a/lib/features/shake_detector/shake_detector_provider.g.dart +++ b/lib/features/shake_detector/shake_detector_provider.g.dart @@ -6,7 +6,7 @@ part of 'shake_detector_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$shakeDetectorHash() => r'8e65e89d59a9cf9492fd5f3eb309eb3a37cf1c6d'; +String _$shakeDetectorHash() => r'c2e6b6b2edf3a40a7a8f5a274f881911be68a5a0'; /// See also [ShakeDetector]. @ProviderFor(ShakeDetector) diff --git a/lib/features/skip_start_end/core/skip_start_end.dart b/lib/features/skip_start_end/core/skip_start_end.dart index 1007774..f4d7e83 100644 --- a/lib/features/skip_start_end/core/skip_start_end.dart +++ b/lib/features/skip_start_end/core/skip_start_end.dart @@ -1,55 +1,55 @@ -import 'dart:async'; +// import 'dart:async'; -import 'package:vaani/features/player/core/abs_audio_player.dart'; -import 'package:vaani/shared/extensions/chapter.dart'; -import 'package:vaani/shared/utils/throttler.dart'; +// import 'package:vaani/features/player/core/abs_audio_player.dart'; +// import 'package:vaani/shared/extensions/chapter.dart'; +// import 'package:vaani/shared/utils/throttler.dart'; -class SkipStartEnd { - final Duration start; - final Duration end; - final AbsAudioPlayer player; +// class SkipStartEnd { +// final Duration start; +// final Duration end; +// final AbsAudioPlayer player; - final List _subscriptions = []; - final throttlerStart = Throttler(delay: Duration(seconds: 3)); - final throttlerEnd = Throttler(delay: Duration(seconds: 3)); +// final List _subscriptions = []; +// final throttlerStart = Throttler(delay: Duration(seconds: 3)); +// final throttlerEnd = Throttler(delay: Duration(seconds: 3)); - SkipStartEnd({ - required this.start, - required this.end, - required this.player, - }) { - if (start > Duration.zero) { - _subscriptions.add( - player.chapterStream.listen((chapter) async { - if (chapter != null && - player.positionInChapter < Duration(seconds: 1)) { - player.seekInBook(chapter.start + start); - } - }), - ); - } - if (end > Duration.zero) { - _subscriptions.add( - player.positionInChapterStream.listen((positionChapter) { - if (end > - (player.currentChapter?.duration ?? Duration.zero) - - positionChapter) { - Future.microtask( - () => throttlerEnd.call(() => player.next()), - ); - } - }), - ); - } - } +// SkipStartEnd({ +// required this.start, +// required this.end, +// required this.player, +// }) { +// if (start > Duration.zero) { +// _subscriptions.add( +// player.chapterStream.listen((chapter) async { +// if (chapter != null && +// player.positionInChapter < Duration(seconds: 1)) { +// player.seekInBook(chapter.start + start); +// } +// }), +// ); +// } +// if (end > Duration.zero) { +// _subscriptions.add( +// player.positionInChapterStream.listen((positionChapter) { +// if (end > +// (player.currentChapter?.duration ?? Duration.zero) - +// positionChapter) { +// Future.microtask( +// () => throttlerEnd.call(() => player.next()), +// ); +// } +// }), +// ); +// } +// } - /// dispose the timer - void dispose() { - for (var sub in _subscriptions) { - sub.cancel(); - } - throttlerStart.dispose(); - throttlerEnd.dispose(); - // _playbackController.close(); - } -} +// /// dispose the timer +// void dispose() { +// for (var sub in _subscriptions) { +// sub.cancel(); +// } +// throttlerStart.dispose(); +// throttlerEnd.dispose(); +// // _playbackController.close(); +// } +// } diff --git a/lib/features/skip_start_end/providers/skip_start_end_provider.dart b/lib/features/skip_start_end/providers/skip_start_end_provider.dart index 1aa9aed..6c30ce9 100644 --- a/lib/features/skip_start_end/providers/skip_start_end_provider.dart +++ b/lib/features/skip_start_end/providers/skip_start_end_provider.dart @@ -1,34 +1,34 @@ -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; -import 'package:vaani/features/player/providers/abs_provider.dart'; -import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core; +// import 'package:riverpod_annotation/riverpod_annotation.dart'; +// import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; +// import 'package:vaani/features/player/providers/abs_provider.dart'; +// import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core; -part 'skip_start_end_provider.g.dart'; +// part 'skip_start_end_provider.g.dart'; -@riverpod -class SkipStartEnd extends _$SkipStartEnd { - @override - core.SkipStartEnd? build() { - final currentBook = ref.watch(currentBookProvider); - final bookId = currentBook?.libraryItemId; - if (currentBook == null || bookId == null) { - return null; - } +// @riverpod +// class SkipStartEnd extends _$SkipStartEnd { +// @override +// core.SkipStartEnd? build() { +// final currentBook = ref.watch(currentBookProvider); +// final bookId = currentBook?.libraryItemId; +// if (currentBook == null || bookId == null) { +// return null; +// } - final player = ref.read(absPlayerProvider); - final bookSettings = ref.watch(bookSettingsProvider(bookId)); - final start = bookSettings.playerSettings.skipChapterStart; - final end = bookSettings.playerSettings.skipChapterEnd; - if (start < Duration.zero && end < Duration.zero) { - return null; - } +// final player = ref.read(absPlayerProvider); +// final bookSettings = ref.watch(bookSettingsProvider(bookId)); +// final start = bookSettings.playerSettings.skipChapterStart; +// final end = bookSettings.playerSettings.skipChapterEnd; +// if (start < Duration.zero && end < Duration.zero) { +// return null; +// } - final skipStartEnd = core.SkipStartEnd( - start: start, - end: end, - player: player, - ); - ref.onDispose(skipStartEnd.dispose); - return skipStartEnd; - } -} +// final skipStartEnd = core.SkipStartEnd( +// start: start, +// end: end, +// player: player, +// ); +// ref.onDispose(skipStartEnd.dispose); +// return skipStartEnd; +// } +// } diff --git a/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart b/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart deleted file mode 100644 index b08cd70..0000000 --- a/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'skip_start_end_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$skipStartEndHash() => r'45572f40a098f081181e8b8bf9e4913e6e649cdc'; - -/// See also [SkipStartEnd]. -@ProviderFor(SkipStartEnd) -final skipStartEndProvider = - AutoDisposeNotifierProvider.internal( - SkipStartEnd.new, - name: r'skipStartEndProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$skipStartEndHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SkipStartEnd = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart index 096ca55..c6349f8 100644 --- a/lib/features/sleep_timer/core/sleep_timer.dart +++ b/lib/features/sleep_timer/core/sleep_timer.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; -import 'package:vaani/features/player/core/abs_audio_player.dart'; /// this timer pauses the music player after a certain duration /// @@ -32,7 +32,7 @@ class SleepTimer { } /// The player to be paused - final AbsAudioPlayer player; + final AudioPlayer player; /// The timer that will pause the player Timer? timer; @@ -50,8 +50,8 @@ class SleepTimer { SleepTimer({required duration, required this.player}) : _duration = duration { _subscriptions.add( player.playerStateStream.listen((event) { - if (event.processingState == AbsProcessingState.completed || - event.processingState == AbsProcessingState.idle) { + if (event.processingState == ProcessingState.completed || + event.processingState == ProcessingState.idle) { clearCountDownTimer(); } }), diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.dart index a4eabe6..92bcd5e 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.dart @@ -26,7 +26,7 @@ class SleepTimer extends _$SleepTimer { var sleepTimer = core.SleepTimer( duration: sleepTimerSettings.defaultDuration, - player: ref.watch(absPlayerProvider), + player: ref.watch(simpleAudioPlayerProvider), ); ref.onDispose(sleepTimer.dispose); return sleepTimer; @@ -45,7 +45,7 @@ class SleepTimer extends _$SleepTimer { } else { final timer = core.SleepTimer( duration: resultingDuration, - player: ref.watch(absPlayerProvider), + player: ref.watch(simpleAudioPlayerProvider), ); ref.onDispose(timer.dispose); state = timer; diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart index c0e59ce..31dad84 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart @@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$sleepTimerHash() => r'417759e07a45e69af93bd9a1c78ac859d9abcf4b'; +String _$sleepTimerHash() => r'7cac4509d8bd40c4d418c295d5b37c66492e7de9'; /// See also [SleepTimer]. @ProviderFor(SleepTimer) diff --git a/lib/main.dart b/lib/main.dart index ca005f2..1c2a886 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.dart'; import 'package:vaani/features/logging/core/logger.dart'; +import 'package:vaani/features/player/core/init.dart'; import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart'; @@ -36,8 +37,8 @@ void main() async { await initStorage(); // initialize audio player - // await configurePlayer(); - await container.read(configurePlayerProvider.future); + await configurePlayer(); + // await container.read(configurePlayerProvider.future); // run the app runApp( UncontrolledProviderScope( @@ -148,10 +149,12 @@ class AbsApp extends ConsumerWidget { final appThemeLight = ThemeData( useMaterial3: true, colorScheme: lightColorScheme.harmonized(), + fontFamily: fontFamilyPlatform, ); final appThemeDark = ThemeData( useMaterial3: true, colorScheme: darkColorScheme.harmonized(), + fontFamily: fontFamilyPlatform, brightness: Brightness.dark, // TODO bottom sheet theme is not working bottomSheetTheme: BottomSheetThemeData( diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index 16b8b23..4d336d2 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -20,6 +20,11 @@ class PlayerPage extends HookConsumerWidget { final isVertical = size.height > size.width; return Scaffold( appBar: AppBar( + // 以下两项确保在滚动后背景色不变 + // elevation: 0 是保持 AppBar 不变的关键 + elevation: 0, + // 设置 forceMaterialTransparency 防止滚动时的透明度变化 + forceMaterialTransparency: true, title: Text(currentBook.metadata.title ?? ''), leading: IconButton( iconSize: 30, diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 35f4ad8..b2321bd 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; // brand color rgb(49, 27, 146) rgb(96, 76, 236) @@ -13,3 +15,18 @@ final brandDarkColorScheme = ColorScheme.fromSeed( seedColor: brandColor, brightness: Brightness.dark, ); + +/// 系统字体(跨平台) +String get fontFamilyPlatform { + if (Platform.isIOS || Platform.isMacOS) { + return 'PingFang SC'; // 苹方,仅苹果设备 + } else if (Platform.isAndroid) { + return 'Roboto'; // Android 默认 + } else if (Platform.isWindows) { + return 'Microsoft YaHei'; // Windows 微软雅黑 + // } else if (Platform.isLinux) { + // return 'Ubuntu'; // Linux + } else { + return 'Arial'; // 其他平台回退 + } +} diff --git a/pubspec.lock b/pubspec.lock index 2f0a775..a91abfd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -770,6 +770,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.5" + just_audio_background: + dependency: "direct main" + description: + path: just_audio_background + ref: media-notification-config + resolved-ref: fce45f334f0838cb6f630548efb65fec40ff17b4 + url: "https://github.com/Dr-Blank/just_audio" + source: git + version: "0.0.1-beta.15" just_audio_media_kit: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index dc56155..7340c36 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,14 +87,15 @@ dependencies: # 音频播放 audio_service: ^0.18.15 + # audio_service_win: ^0.0.2 audio_session: ^0.1.23 just_audio: ^0.10.5 - # just_audio_background: - # # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed - # git: - # url: https://github.com/Dr-Blank/just_audio - # ref: media-notification-config - # path: just_audio_background + just_audio_background: + # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed + git: + url: https://github.com/Dr-Blank/just_audio + ref: media-notification-config + path: just_audio_background # just_audio_windows: ^0.2.2 just_audio_media_kit: ^2.0.4 media_kit_libs_linux: any