From 420438c0df1689b443038ec98e2d8949307d519e Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Mon, 8 Dec 2025 17:54:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=92=AD=E6=94=BE=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/library_item_actions.dart | 13 +- .../player/core/abs_audio_handler.dart | 79 ++--- lib/features/player/core/init.dart | 33 ++ .../player/providers/abs_provider.dart | 243 +++++-------- .../player/providers/abs_provider.g.dart | 110 ++++-- .../providers/currently_playing_provider.dart | 143 ++++---- .../currently_playing_provider.g.dart | 59 ---- lib/features/player/view/player_expanded.dart | 2 +- .../player/view/player_expanded_desktop.dart | 6 +- .../player/view/player_minimized.dart | 23 +- .../widgets/audiobook_player_seek_button.dart | 4 +- .../audiobook_player_seek_chapter_button.dart | 15 +- .../widgets/chapter_selection_button.dart | 2 +- .../widgets/player_player_pause_button.dart | 62 ++-- .../view/widgets/player_progress_bar.dart | 12 +- .../widgets/player_speed_adjust_button.dart | 9 +- .../providers/skip_start_end_provider.dart | 2 +- .../view/skip_start_end_button.dart | 2 +- lib/framework.dart | 10 +- lib/main.dart | 5 +- lib/pages/player_page.dart | 12 +- lib/router/scaffold_with_nav_bar.dart | 4 +- lib/shared/audio_player.dart | 318 +++++++++++++++++- lib/shared/audio_player_mpv.dart | 107 ++++++ lib/shared/widgets/drawer.dart | 13 +- lib/shared/widgets/shelves/book_shelf.dart | 18 +- lib/shared/widgets/tray_manager.dart | 8 +- pubspec.lock | 8 - pubspec.yaml | 2 +- 29 files changed, 810 insertions(+), 514 deletions(-) create mode 100644 lib/features/player/core/init.dart delete mode 100644 lib/features/player/providers/currently_playing_provider.g.dart create mode 100644 lib/shared/audio_player_mpv.dart diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 31e0037..1b652e3 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -14,13 +14,13 @@ import 'package:vaani/features/downloads/providers/download_manager.dart' isItemDownloadingProvider, itemDownloadProgressProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; -import 'package:vaani/features/player/providers/player_status_provider.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/player_status_provider.dart'; +import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/utils.dart'; @@ -470,9 +470,10 @@ class _LibraryItemPlayButton extends HookConsumerWidget { onPressed: () { currentBook?.libraryItemId == book.libraryItemId ? ref.read(playerProvider).togglePlayPause() - : ref - .read(currentBookProvider.notifier) - .update(book, userMediaProgress?.currentTime); + : ref.read(absAudioPlayerProvider.notifier).load( + book, + initialPosition: userMediaProgress?.currentTime, + ); }, icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + book.libraryItemId, diff --git a/lib/features/player/core/abs_audio_handler.dart b/lib/features/player/core/abs_audio_handler.dart index 7167f07..b7d1fdd 100644 --- a/lib/features/player/core/abs_audio_handler.dart +++ b/lib/features/player/core/abs_audio_handler.dart @@ -1,17 +1,22 @@ import 'package:audio_service/audio_service.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:vaani/features/player/providers/abs_provider.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart' + hide AbsAudioPlayer; +import 'package:vaani/shared/audio_player.dart'; class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { - final Player player = Player(); + final AbsAudioPlayer player; - AbsAudioHandler(Ref ref) { + AbsAudioHandler(this.player) { + player.mediaItemStream.listen((item) { + mediaItem.add(item); + }); playbackState.add( playbackState.value.copyWith( controls: [ MediaControl.skipToPrevious, - if (player.state.playing) MediaControl.pause else MediaControl.play, + // if (player.state.playing) MediaControl.pause else MediaControl.play, // MediaControl.rewind, // MediaControl.fastForward, MediaControl.skipToNext, @@ -27,48 +32,40 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { ), ); - final absState = ref.read(absStateProvider.notifier); // 1. 转发播放/暂停状态 - player.stream.playing.listen((bool playing) { - playbackState.add(playbackState.value.copyWith( - playing: playing, - // 根据 playing 和实际情况更新 processingState - processingState: player.state.completed - ? AudioProcessingState.completed - : player.state.buffering - ? AudioProcessingState.buffering - : AudioProcessingState.ready, - )); - absState.updataPlaying(playing); + player.playerStateStream.listen((playerState) { + playbackState.add( + playbackState.value.copyWith( + playing: playerState.playing, + // 根据 playing 和实际情况更新 processingState + processingState: const { + ProcessingState.idle: AudioProcessingState.idle, + ProcessingState.loading: AudioProcessingState.loading, + ProcessingState.buffering: AudioProcessingState.buffering, + ProcessingState.ready: AudioProcessingState.ready, + ProcessingState.completed: AudioProcessingState.completed, + }[playerState.processingState] ?? + AudioProcessingState.idle, + ), + ); }); // 2. 转发播放位置 - player.stream.position.listen((Duration position) { + 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('播放完成'); - }); - } - - Future playOrPause() async { - await player.playOrPause(); - } - - Future setMediaItem(MediaItem mediaItem) async { - this.mediaItem.add(mediaItem); - await player.open(Media(mediaItem.id), play: false); - // ignore: unnecessary_null_comparison - await player.stream.duration.firstWhere((d) => d != null); + // 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('播放完成'); + // }); } // 播放控制方法重写 @@ -99,14 +96,10 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { @override Future setSpeed(double speed) async { - await player.setRate(speed); + await player.setSpeed(speed); } Future setVolume(double volume) async { - final state = player.state; await player.setVolume(volume); } - - PlayerStream get stream => player.stream; - PlayerState get state => player.state; } diff --git a/lib/features/player/core/init.dart b/lib/features/player/core/init.dart new file mode 100644 index 0000000..014e7ef --- /dev/null +++ b/lib/features/player/core/init.dart @@ -0,0 +1,33 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:vaani/features/player/core/abs_audio_handler.dart' as core; +import 'package:vaani/features/player/providers/abs_provider.dart'; +import 'package:vaani/globals.dart'; + +Future configurePlayer(ProviderContainer container) async { + // for playing audio on windows, linux + MediaKit.ensureInitialized(); + // for configuring how this app will interact with other audio apps + final session = await AudioSession.instance; + await session.configure(const AudioSessionConfiguration.speech()); + + await AudioService.init( + builder: () => core.AbsAudioHandler(container.read(absAudioPlayerProvider)), + 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), + ), + ); + + appLogger.finer('created simple player'); +} diff --git a/lib/features/player/providers/abs_provider.dart b/lib/features/player/providers/abs_provider.dart index ca52abc..438c1dc 100644 --- a/lib/features/player/providers/abs_provider.dart +++ b/lib/features/player/providers/abs_provider.dart @@ -1,192 +1,111 @@ -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:logging/logging.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as api; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/library_item_provider.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' as core; -import 'package:vaani/features/player/core/abs_audio_player.dart' as core; -import 'package:vaani/features/player/core/audiobook_player.dart'; -import 'package:vaani/features/settings/app_settings_provider.dart'; -import 'package:vaani/globals.dart'; -import 'package:vaani/shared/extensions/chapter.dart'; +import 'package:vaani/shared/audio_player.dart' as core; +import 'package:vaani/shared/audio_player_mpv.dart'; part 'abs_provider.g.dart'; final _logger = Logger('AbsPlayerProvider'); @Riverpod(keepAlive: true) -Future absAudioHandlerInit(Ref ref) async { - // for playing audio on windows, linux - MediaKit.ensureInitialized(); - // 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: () => core.AbsAudioHandler(ref), - 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; -} - -@Riverpod(keepAlive: true) -class AbsPlayer extends _$AbsPlayer { +class AbsAudioPlayer extends _$AbsAudioPlayer { @override - core.AbsAudioHandler build() { - return ref.read(absAudioHandlerInitProvider).valueOrNull!; - } -} - -@Riverpod(keepAlive: true) -class AbsState extends _$AbsState { - @override - core.AbsPlayerState build() { - return core.AbsPlayerState(); + core.AbsAudioPlayer build() { + final player = AbsMpvAudioPlayer(); + return player; } - // 加载书籍 - Future load(api.BookExpanded book, Duration? currentTime) async { - final player = ref.read(absPlayerProvider); - if (state.book == book || state.book?.libraryItemId == book.libraryItemId) { - appLogger.info('Book was already set'); - player.playOrPause(); + Future load( + api.BookExpanded book, { + Duration? initialPosition, + }) async { + if (state.book == book) { + state.playOrPause(); return; } - - final appSettings = ref.read(appSettingsProvider); final api = ref.read(authenticatedApiProvider); - final downloadManager = ref.read(simpleDownloadManagerProvider); - final libItem = - await ref.read(libraryItemProvider(book.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); - - var bookPlayerSettings = - ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings; - var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - - var configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; - final trackToPlay = book.findTrackAtTime(currentTime ?? Duration.zero); - final chapterToPlay = book.findChapterAtTime(currentTime ?? Duration.zero); - final initialIndex = book.tracks.indexOf(trackToPlay); - final initialPositionInTrack = - currentTime != null ? currentTime - trackToPlay.startOffset : null; - final album = appSettings.notificationSettings.primaryTitle - .formatNotificationTitle(book); - final artlist = appSettings.notificationSettings.secondaryTitle - .formatNotificationTitle(book); - - final id = _getUri(trackToPlay, downloadedUris, - baseUrl: api.baseUrl, token: api.token!) - .toString(); - final item = MediaItem( - id: id, - title: chapterToPlay.title, - album: album, - artist: artlist, - duration: chapterToPlay.duration, - artUri: Uri.parse( - '${api.baseUrl}/api/items/${book.libraryItemId}/cover?token=${api.token!}', - ), + await state.load( + book, + baseUrl: api.baseUrl, + token: api.token!, + initialPosition: initialPosition, ); - - state = state.copyWith( - book: book, - currentChapter: chapterToPlay, - currentIndex: initialIndex, - ); - player.playMediaItem(item); - await Future.wait([ - player.setMediaItem(item), - // player.setVolume( - // configurePlayerForEveryBook - // ? bookPlayerSettings.preferredDefaultVolume ?? - // appPlayerSettings.preferredDefaultVolume - // : appPlayerSettings.preferredDefaultVolume, - // ), - player.setSpeed( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed - : appPlayerSettings.preferredDefaultSpeed, - ), - player.play(), - ]); - - // player.setSourceAudiobook( - // book, - // baseUrl: api.baseUrl, - // token: api.token!, - // initialPosition: currentTime, - // downloadedUris: downloadedUris, - // volume: configurePlayerForEveryBook - // ? bookPlayerSettings.preferredDefaultVolume ?? - // appPlayerSettings.preferredDefaultVolume - // : appPlayerSettings.preferredDefaultVolume, - // speed: configurePlayerForEveryBook - // ? bookPlayerSettings.preferredDefaultSpeed ?? - // appPlayerSettings.preferredDefaultSpeed - // : appPlayerSettings.preferredDefaultSpeed, - // ); + await state.play(); } +} - Future next() async {} - - Future previous() async {} - void updataPlaying(bool playing) { - state = state.copyWith(playing: playing); - } - - Stream get positionStreamInChapter { - final player = ref.read(absPlayerProvider); - - return player.stream.position.distinct().map((position) { - return position + - (state.book?.tracks[state.currentIndex].startOffset ?? - Duration.zero) - - (state.currentChapter?.start ?? Duration.zero); +@riverpod +class PlayerState extends _$PlayerState { + @override + core.PlayerState build() { + final player = ref.read(absAudioPlayerProvider); + player.playerStateStream.listen((playerState) { + if (playerState != state) { + state = playerState; + } }); + return player.playerState; } +} - 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; - }, - ); +@riverpod +class CurrentBook extends _$CurrentBook { + @override + api.BookExpanded? build() { + final player = ref.read(absAudioPlayerProvider); + player.bookStream.listen((book) { + if (book != state) { + state = book; + } + }); + return player.book; + } +} - return uri ?? - Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); +@riverpod +bool isPlayerActive(Ref ref) { + final player = ref.read(absAudioPlayerProvider); + player.bookStream.listen((book) { + ref.invalidateSelf(); + }); + return player.book != null; +} + +@riverpod +class CurrentChapter extends _$CurrentChapter { + @override + api.BookChapter? build() { + final player = ref.read(absAudioPlayerProvider); + player.chapterStream.listen((chapter) { + if (chapter != state) { + state = chapter; + } + }); + return player.currentChapter; } } @riverpod Stream positionChapter(Ref ref) { - return ref.watch(absStateProvider.notifier).positionStreamInChapter; + return ref.read(absAudioPlayerProvider).positionInChapterStream; +} + +@riverpod +List currentChapters(Ref ref) { + final book = ref.watch(currentBookProvider); + if (book == null) { + return []; + } + final currentChapter = ref.watch(currentChapterProvider); + if (currentChapter == null) { + return []; + } + final index = book.chapters.indexOf(currentChapter); + final total = book.chapters.length; + final start = index - 3 >= 0 ? index - 3 : 0; + final end = start + 20 <= total ? start + 20 : total; + return book.chapters.sublist(start, end); } diff --git a/lib/features/player/providers/abs_provider.g.dart b/lib/features/player/providers/abs_provider.g.dart index aaa0409..d76ccd1 100644 --- a/lib/features/player/providers/abs_provider.g.dart +++ b/lib/features/player/providers/abs_provider.g.dart @@ -6,26 +6,24 @@ part of 'abs_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$absAudioHandlerInitHash() => - r'bb46f715e9d51bb6269d0d77e314665601a6bdb0'; +String _$isPlayerActiveHash() => r'52fc689deeba4d21a33a73290d297f128324ae99'; -/// See also [absAudioHandlerInit]. -@ProviderFor(absAudioHandlerInit) -final absAudioHandlerInitProvider = - FutureProvider.internal( - absAudioHandlerInit, - name: r'absAudioHandlerInitProvider', +/// See also [isPlayerActive]. +@ProviderFor(isPlayerActive) +final isPlayerActiveProvider = AutoDisposeProvider.internal( + isPlayerActive, + name: r'isPlayerActiveProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$absAudioHandlerInitHash, + : _$isPlayerActiveHash, dependencies: null, allTransitiveDependencies: null, ); @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef AbsAudioHandlerInitRef = FutureProviderRef; -String _$positionChapterHash() => r'b1d19345bceb2e54399e15fbb16a534f4be5ba43'; +typedef IsPlayerActiveRef = AutoDisposeProviderRef; +String _$positionChapterHash() => r'68f7ca4df2ac6f6f78a645d98e2dbfaf2ffe46bf'; /// See also [positionChapter]. @ProviderFor(positionChapter) @@ -42,35 +40,85 @@ final positionChapterProvider = AutoDisposeStreamProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef PositionChapterRef = AutoDisposeStreamProviderRef; -String _$absPlayerHash() => r'c313a2456bb221b83f3cd2142ae63d6463ef304b'; +String _$currentChaptersHash() => r'2d694aaa17f7eed8f97859d83e5b61f22966c35a'; -/// See also [AbsPlayer]. -@ProviderFor(AbsPlayer) -final absPlayerProvider = - NotifierProvider.internal( - AbsPlayer.new, - name: r'absPlayerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$absPlayerHash, +/// See also [currentChapters]. +@ProviderFor(currentChapters) +final currentChaptersProvider = + AutoDisposeProvider>.internal( + currentChapters, + name: r'currentChaptersProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentChaptersHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$AbsPlayer = Notifier; -String _$absStateHash() => r'6b4ca07c7998304a1522a07b23955c3e54a441e3'; +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef CurrentChaptersRef = AutoDisposeProviderRef>; +String _$absAudioPlayerHash() => r'04636b3275f16747eeeb008c8b4dda4e8a1f8ed2'; -/// See also [AbsState]. -@ProviderFor(AbsState) -final absStateProvider = - NotifierProvider.internal( - AbsState.new, - name: r'absStateProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$absStateHash, +/// 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 _$AbsState = Notifier; +typedef _$AbsAudioPlayer = Notifier; +String _$playerStateHash() => r'ed07c487fdad5fd0e21dfd32a274eecc470e83a4'; + +/// See also [PlayerState]. +@ProviderFor(PlayerState) +final playerStateProvider = + AutoDisposeNotifierProvider.internal( + PlayerState.new, + name: r'playerStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playerStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PlayerState = AutoDisposeNotifier; +String _$currentBookHash() => r'40c24ad45aab37afc32e8e8383d6abbe19b714bc'; + +/// See also [CurrentBook]. +@ProviderFor(CurrentBook) +final currentBookProvider = + AutoDisposeNotifierProvider.internal( + CurrentBook.new, + name: r'currentBookProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$currentBookHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$CurrentBook = AutoDisposeNotifier; +String _$currentChapterHash() => r'89868a72b106e0916883ee92bf3d18650288c586'; + +/// See also [CurrentChapter]. +@ProviderFor(CurrentChapter) +final currentChapterProvider = + AutoDisposeNotifierProvider.internal( + CurrentChapter.new, + name: r'currentChapterProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentChapterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$CurrentChapter = 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/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index 258a54f..12bb78d 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -9,89 +9,72 @@ import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/globals.dart'; -part 'currently_playing_provider.g.dart'; +// part 'currently_playing_provider.g.dart'; -@riverpod -class CurrentBook extends _$CurrentBook { - @override - core.BookExpanded? build() { - return null; - } +// @riverpod +// class CurrentBook extends _$CurrentBook { +// @override +// core.BookExpanded? build() { +// return null; +// } - Future update(core.BookExpanded book, Duration? currentTime) async { - final audioService = ref.read(playerProvider); - if (state == book) { - appLogger.info('Book was already set'); - if (audioService.player.playing) { - appLogger.info('Pausing the book'); - await audioService.pause(); - return; - } else { - await audioService.play(); - } - } - state = book; - final api = ref.read(authenticatedApiProvider); - final downloadManager = ref.read(simpleDownloadManagerProvider); - final libItem = - await ref.read(libraryItemProvider(state!.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); +// Future update(core.BookExpanded book, Duration? currentTime) async { +// final audioService = ref.read(playerProvider); +// if (state == book) { +// appLogger.info('Book was already set'); +// if (audioService.player.playing) { +// appLogger.info('Pausing the book'); +// await audioService.pause(); +// return; +// } else { +// await audioService.play(); +// } +// } +// state = book; +// final api = ref.read(authenticatedApiProvider); +// final downloadManager = ref.read(simpleDownloadManagerProvider); +// final libItem = +// await ref.read(libraryItemProvider(state!.libraryItemId).future); +// final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); - var bookPlayerSettings = - ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings; - var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; +// var bookPlayerSettings = +// ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings; +// var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - var configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; - audioService.setSourceAudiobook( - state!, - baseUrl: api.baseUrl, - token: api.token!, - initialPosition: currentTime, - downloadedUris: downloadedUris, - volume: configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultVolume ?? - appPlayerSettings.preferredDefaultVolume - : appPlayerSettings.preferredDefaultVolume, - speed: configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed - : appPlayerSettings.preferredDefaultSpeed, - ); - } -} +// var configurePlayerForEveryBook = +// appPlayerSettings.configurePlayerForEveryBook; +// audioService.setSourceAudiobook( +// state!, +// baseUrl: api.baseUrl, +// token: api.token!, +// initialPosition: currentTime, +// downloadedUris: downloadedUris, +// volume: configurePlayerForEveryBook +// ? bookPlayerSettings.preferredDefaultVolume ?? +// appPlayerSettings.preferredDefaultVolume +// : appPlayerSettings.preferredDefaultVolume, +// speed: configurePlayerForEveryBook +// ? bookPlayerSettings.preferredDefaultSpeed ?? +// appPlayerSettings.preferredDefaultSpeed +// : appPlayerSettings.preferredDefaultSpeed, +// ); +// } +// } -@riverpod -class CurrentChapter extends _$CurrentChapter { - @override - core.BookChapter? build() { - final player = ref.watch(playerProvider); - player.chapterStream.distinct().listen((chapter) { - update(chapter); - }); - return player.currentChapter; - } +// @riverpod +// class CurrentChapter extends _$CurrentChapter { +// @override +// core.BookChapter? build() { +// final player = ref.watch(playerProvider); +// player.chapterStream.distinct().listen((chapter) { +// update(chapter); +// }); +// return player.currentChapter; +// } - void update(core.BookChapter? chapter) { - if (state != chapter) { - state = chapter; - } - } -} - -@riverpod -List currentChapters(Ref ref) { - final book = ref.watch(currentBookProvider); - if (book == null) { - return []; - } - final currentChapter = ref.watch(currentChapterProvider); - if (currentChapter == null) { - return []; - } - final index = book.chapters.indexOf(currentChapter); - final total = book.chapters.length; - final start = index - 3 >= 0 ? index - 3 : 0; - final end = start + 20 <= total ? start + 20 : total; - return book.chapters.sublist(start, end); -} +// void update(core.BookChapter? chapter) { +// if (state != chapter) { +// state = chapter; +// } +// } +// } diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart deleted file mode 100644 index c592986..0000000 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ /dev/null @@ -1,59 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'currently_playing_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentChaptersHash() => r'6574b3f4ee0af8006f233aaf76cc507d188c6305'; - -/// See also [currentChapters]. -@ProviderFor(currentChapters) -final currentChaptersProvider = - AutoDisposeProvider>.internal( - currentChapters, - name: r'currentChaptersProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentChaptersHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef CurrentChaptersRef = AutoDisposeProviderRef>; -String _$currentBookHash() => r'5143d08375c2c58918e82f8d368998bb38d7b790'; - -/// See also [CurrentBook]. -@ProviderFor(CurrentBook) -final currentBookProvider = - AutoDisposeNotifierProvider.internal( - CurrentBook.new, - name: r'currentBookProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$currentBookHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$CurrentBook = AutoDisposeNotifier; -String _$currentChapterHash() => r'f5f6d9e49cb7e455d032f7370f364d9ce30b8eb1'; - -/// See also [CurrentChapter]. -@ProviderFor(CurrentChapter) -final currentChapterProvider = - AutoDisposeNotifierProvider.internal( - CurrentChapter.new, - name: r'currentChapterProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentChapterHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$CurrentChapter = 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/player/view/player_expanded.dart b/lib/features/player/view/player_expanded.dart index 5cceffc..36b302e 100644 --- a/lib/features/player/view/player_expanded.dart +++ b/lib/features/player/view/player_expanded.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_progress_bar.dart'; import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart'; diff --git a/lib/features/player/view/player_expanded_desktop.dart b/lib/features/player/view/player_expanded_desktop.dart index 94e68e5..095d6a7 100644 --- a/lib/features/player/view/player_expanded_desktop.dart +++ b/lib/features/player/view/player_expanded_desktop.dart @@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/constants/sizes.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart' show PlayerExpandedImage; import 'package:vaani/features/player/view/player_minimized.dart'; @@ -74,9 +74,9 @@ class PlayerExpandedDesktop extends HookConsumerWidget { // buttonSkipBackwards const AudiobookPlayerSeekButton(isForward: false), AudiobookPlayerPlayPauseButton(), - // buttonSkipForwards + // // buttonSkipForwards const AudiobookPlayerSeekButton(isForward: true), - // next chapter + // // next chapter const AudiobookPlayerSeekChapterButton( isForward: true), ], diff --git a/lib/features/player/view/player_minimized.dart b/lib/features/player/view/player_minimized.dart index fc8a925..52ae52b 100644 --- a/lib/features/player/view/player_minimized.dart +++ b/lib/features/player/view/player_minimized.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 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/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.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'; @@ -21,12 +18,11 @@ class PlayerMinimized extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentBook = ref.watch(absStateProvider.select((v) => v.book)); + final currentBook = ref.watch(currentBookProvider); if (currentBook == null) { return SizedBox.shrink(); } - final currentChapter = - ref.watch(absStateProvider.select((v) => v.currentChapter)); + final currentChapter = ref.watch(currentChapterProvider); return PlayerMinimizedFramework( children: [ @@ -63,14 +59,14 @@ class PlayerMinimized extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ // AutoScrollText( - PlatformText( + Text( '${currentBook.metadata.title ?? ''} - ${currentChapter?.title ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, // velocity: // const Velocity(pixelsPerSecond: Offset(16, 0)), style: Theme.of(context).textTheme.bodyLarge, ), - PlatformText( + Text( currentBook.metadata.asBookMetadataExpanded.authorName ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, @@ -89,7 +85,7 @@ class PlayerMinimized extends HookConsumerWidget { // rewind button Padding( padding: const EdgeInsets.only(left: 8), - child: PlatformIconButton( + child: IconButton( icon: const Icon( Icons.replay_30, size: AppElementSizes.iconSizeSmall, @@ -116,13 +112,14 @@ class PlayerMinimizedFramework extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // final player = ref.watch(playerProvider); - final currentChapter = - ref.watch(absStateProvider.select((v) => v.currentChapter)); + final currentChapter = ref.watch(currentChapterProvider); final progress = // useStream(player.positionStreamInChapter, initialData: Duration.zero); - useStream(ref.read(absStateProvider.notifier).positionStreamInChapter, - initialData: Duration.zero); + useStream( + ref.read(absAudioPlayerProvider).positionInChapterStream, + initialData: Duration.zero, + ); return GestureDetector( onTap: () { if (GoRouterState.of(context).topRoute?.name != Routes.player.name) { diff --git a/lib/features/player/view/widgets/audiobook_player_seek_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_button.dart index 43d4ffa..db5f5d2 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; class AudiobookPlayerSeekButton extends HookConsumerWidget { const AudiobookPlayerSeekButton({ @@ -14,7 +14,7 @@ class AudiobookPlayerSeekButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(playerProvider); + final player = ref.read(absAudioPlayerProvider); return IconButton( icon: Icon( isForward ? Icons.forward_30 : Icons.replay_30, diff --git a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart index 6a84d2d..c8450c2 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { const AudiobookPlayerSeekChapterButton({ @@ -14,25 +14,26 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(playerProvider); return IconButton( icon: Icon( isForward ? Icons.skip_next : Icons.skip_previous, size: AppElementSizes.iconSizeSmall, ), onPressed: () { - if (player.book == null) { + final player = ref.read(absAudioPlayerProvider); + final book = ref.read(currentBookProvider); + if (book == null) { return; } // if chapter does not exist, go to the start or end of the book - if (player.currentChapter == null) { - player.seekInBook(isForward ? player.book!.duration : Duration.zero); + if (ref.read(currentChapterProvider) == null) { + player.seekInBook(isForward ? book.duration : Duration.zero); return; } if (isForward) { - player.skipToNext(); + player.next(); } else { - player.skipToPrevious(); + player.previous(); } }, ); diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index d8b6a12..6e934cd 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart' show pendingPlayerModals; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; 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 e850bb2..f921375 100644 --- a/lib/features/player/view/widgets/player_player_pause_button.dart +++ b/lib/features/player/view/widgets/player_player_pause_button.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/core/player_status.dart'; -import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart' + hide PlayerState; +import 'package:vaani/shared/audio_player.dart'; class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { const AudiobookPlayerPlayPauseButton({ @@ -14,42 +13,39 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { final double iconSize; @override Widget build(BuildContext context, WidgetRef ref) { - final playerStatus = - ref.watch(playerStatusProvider.select((v) => v.playStatus)); + final playerState = ref.watch(playerStateProvider); - return PlatformIconButton( - icon: _getIcon(playerStatus, context), - onPressed: () => _actionButtonPressed(playerStatus, ref), + return IconButton( + icon: _getIcon(playerState, context), + onPressed: () => _actionButtonPressed(playerState, ref), ); } - Widget _getIcon(PlayStatus playerStatus, BuildContext context) { - switch (playerStatus) { - case PlayStatus.playing: - return Icon(size: iconSize, PlatformIcons(context).pause); - case PlayStatus.paused: - return Icon(size: iconSize, PlatformIcons(context).playArrow); - case PlayStatus.loading: - return PlatformCircularProgressIndicator(); - default: - return Icon(size: iconSize, PlatformIcons(context).playArrow); + Widget _getIcon(PlayerState playerState, BuildContext context) { + if (playerState.playing) { + return Icon(size: iconSize, Icons.pause); + } else { + switch (playerState.processingState) { + case ProcessingState.loading || ProcessingState.buffering: + return CircularProgressIndicator(); + default: + return Icon(size: iconSize, Icons.play_arrow); + } } } - void _actionButtonPressed(PlayStatus playerStatus, WidgetRef ref) async { - final player = ref.read(playerProvider); - switch (playerStatus) { - case PlayStatus.loading: - break; - case PlayStatus.playing: - await player.pause(); - break; - case PlayStatus.completed: - await player.seekInBook(const Duration(seconds: 0)); - await player.play(); - break; - default: - await player.play(); + void _actionButtonPressed(PlayerState playerState, WidgetRef ref) async { + final player = ref.read(absAudioPlayerProvider); + if (playerState.playing) { + await player.pause(); + } else { + switch (playerState.processingState) { + case ProcessingState.completed: + await player.seekInBook(const Duration(seconds: 0)); + await player.play(); + default: + await player.play(); + } } } } diff --git a/lib/features/player/view/widgets/player_progress_bar.dart b/lib/features/player/view/widgets/player_progress_bar.dart index 9675732..34823fe 100644 --- a/lib/features/player/view/widgets/player_progress_bar.dart +++ b/lib/features/player/view/widgets/player_progress_bar.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.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/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; class AudiobookChapterProgressBar extends HookConsumerWidget { const AudiobookChapterProgressBar({ @@ -13,14 +13,14 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(playerProvider); + final player = ref.watch(absAudioPlayerProvider); final currentChapter = ref.watch(currentChapterProvider); final position = useStream( - player.positionStreamInBook, + player.positionInBookStream, initialData: const Duration(seconds: 0), ); final buffered = useStream( - player.bufferedPositionStreamInBook, + player.bufferedPositionInBookStream, initialData: const Duration(seconds: 0), ); @@ -64,9 +64,9 @@ class AudiobookProgressBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(playerProvider); + final player = ref.read(absAudioPlayerProvider); final position = useStream( - player.slowPositionStreamInBook, + player.positionInBookStream, initialData: const Duration(seconds: 0), ); diff --git a/lib/features/player/view/widgets/player_speed_adjust_button.dart b/lib/features/player/view/widgets/player_speed_adjust_button.dart index 43925a6..c3f0618 100644 --- a/lib/features/player/view/widgets/player_speed_adjust_button.dart +++ b/lib/features/player/view/widgets/player_speed_adjust_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/features/player/view/widgets/speed_selector.dart'; import 'package:vaani/features/settings/app_settings_provider.dart'; @@ -16,12 +16,13 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(playerProvider); - final bookId = player.book?.libraryItemId ?? '_'; + final player = ref.read(absAudioPlayerProvider); + final book = ref.read(currentBookProvider); + final bookId = book?.libraryItemId ?? '_'; final bookSettings = ref.watch(bookSettingsProvider(bookId)); final appSettings = ref.watch(appSettingsProvider); return TextButton( - child: Text('${player.player.speed}x'), + child: Text('${player.speed}x'), onPressed: () async { pendingPlayerModals++; _logger.fine('opening speed selector'); 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 e237b94..f05a51b 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,7 +1,7 @@ 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/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core; part 'skip_start_end_provider.g.dart'; diff --git a/lib/features/skip_start_end/view/skip_start_end_button.dart b/lib/features/skip_start_end/view/skip_start_end_button.dart index 2e2be51..a0eea44 100644 --- a/lib/features/skip_start_end/view/skip_start_end_button.dart +++ b/lib/features/skip_start_end/view/skip_start_end_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/features/settings/view/notification_settings_page.dart'; import 'package:vaani/generated/l10n.dart'; diff --git a/lib/framework.dart b/lib/framework.dart index 2eb8d1f..0bab5b4 100644 --- a/lib/framework.dart +++ b/lib/framework.dart @@ -18,11 +18,11 @@ class Framework extends ConsumerWidget { // Eagerly initialize providers by watching them. // By using "watch", the provider will stay alive and not be disposed. try { - ref.watch(simpleDownloadManagerProvider); - if (Helper.isAndroid()) ref.watch(shakeDetectorProvider); - ref.watch(sleepTimerProvider); - ref.watch(skipStartEndProvider); - ref.watch(playbackReporterProvider); + // ref.watch(simpleDownloadManagerProvider); + // if (Helper.isAndroid()) ref.watch(shakeDetectorProvider); + // ref.watch(sleepTimerProvider); + // ref.watch(skipStartEndProvider); + // ref.watch(playbackReporterProvider); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); appLogger.severe(e.toString()); diff --git a/lib/main.dart b/lib/main.dart index 58df9a2..f6e103c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,8 +8,8 @@ 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/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/framework.dart'; @@ -38,8 +38,7 @@ void main() async { // initialize audio player // await configurePlayer(); - // await container.read(audioHandlerInitProvider.future); - await container.read(absAudioHandlerInitProvider.future); + await configurePlayer(container); // run the app runApp( UncontrolledProviderScope( diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index de9b6d6..bb4b2b6 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/features/player/view/player_expanded_desktop.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; @@ -19,15 +18,15 @@ class PlayerPage extends HookConsumerWidget { final size = MediaQuery.of(context).size; // 竖屏 final isVertical = size.height > size.width; - return PlatformScaffold( - appBar: PlatformAppBar( + return Scaffold( + appBar: AppBar( title: Text(currentBook.metadata.title ?? ''), leading: IconButton( iconSize: 30, icon: const Icon(Icons.keyboard_arrow_down), onPressed: () => context.pop(), ), - trailingActions: [ + actions: [ IconButton( icon: const Icon(Icons.cast), onPressed: () { @@ -36,7 +35,8 @@ class PlayerPage extends HookConsumerWidget { ), ], ), - body: isVertical ? PlayerExpanded() : PlayerExpandedDesktop(), + // body: isVertical ? PlayerExpanded() : PlayerExpandedDesktop(), + body: PlayerExpanded(), ); } } diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index e1c964a..a34d991 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/features/explore/providers/search_controller.dart'; import 'package:vaani/features/player/providers/abs_provider.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/view/player_minimized.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/generated/l10n.dart'; @@ -51,8 +50,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget { Widget buildNavLeft(BuildContext context, WidgetRef ref) { // final isPlayerActive = ref.watch(isPlayerActiveProvider); - // final currentBook = ref.watch(currentBookProvider); - final currentBook = ref.watch(absStateProvider.select((v) => v.book)); + final currentBook = ref.watch(currentBookProvider); return Padding( padding: EdgeInsets.only(bottom: currentBook != null ? playerMinHeight : 0), diff --git a/lib/shared/audio_player.dart b/lib/shared/audio_player.dart index b2ef502..5df416b 100644 --- a/lib/shared/audio_player.dart +++ b/lib/shared/audio_player.dart @@ -1,27 +1,152 @@ +// 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:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart'; + +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; +import 'package:vaani/shared/extensions/chapter.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; + +final offset = Duration(milliseconds: 10); + +final _logger = Logger('AbsAudioPlayer'); abstract class AbsAudioPlayer { - AbsAudioPlayer._(); + final _mediaItemController = BehaviorSubject.seeded(null); + final playerStateSubject = + BehaviorSubject.seeded(PlayerState(false, ProcessingState.idle)); + final _bookStreamController = BehaviorSubject.seeded(null); + final _chapterStreamController = BehaviorSubject.seeded(null); - BookExpanded? _book; + BookExpanded? get book => _bookStreamController.nvalue; + BookChapter? get currentChapter => _chapterStreamController.nvalue; + PlayerState get playerState => playerStateSubject.value; + Stream get mediaItemStream => _mediaItemController.stream; + Stream get playerStateStream => playerStateSubject.stream; - BookExpanded? get book => _book; + Future load( + BookExpanded book, { + required Uri baseUrl, + required String token, + Duration? initialPosition, + List? downloadedUris, + }) async { + if (_bookStreamController.nvalue == book) { + _logger.info('Book is the same, doing nothing'); + return; + } + _bookStreamController.add(book); + final appSettings = loadOrCreateAppSettings(); + final currentTrack = book.findTrackAtTime(initialPosition ?? Duration.zero); + final indexTrack = book.tracks.indexOf(currentTrack); + final positionInTrack = initialPosition != null + ? initialPosition - currentTrack.startOffset + : null; + final title = appSettings.notificationSettings.primaryTitle + .formatNotificationTitle(book); + final artist = appSettings.notificationSettings.secondaryTitle + .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(currentTrack, downloadedUris, + baseUrl: baseUrl, token: token), + ) + .toList(); + setPlayList(playlist, index: indexTrack, position: positionInTrack); + } + + Future setPlayList( + List playlist, { + int? index, + Duration? position, + }); Future play(); Future pause(); Future playOrPause(); - Future next(); - Future previous(); + + // 跳到下一章 + Future next() async { + final chapter = currentChapter; + if (book == null || chapter == null) { + return; + } + final chapterIndex = book!.chapters.indexOf(chapter); + if (chapterIndex < book!.chapters.length - 1) { + final nextChapter = book!.chapters[chapterIndex + 1]; + await switchChapter(nextChapter.id); + } + } + + // 跳到上一章 + Future previous() async { + final chapter = currentChapter; + if (book == null || chapter == null) { + return; + } + final currentIndex = book!.chapters.indexOf(chapter); + if (currentIndex > 0) { + final prevChapter = book!.chapters[currentIndex - 1]; + await switchChapter(prevChapter.id); + } else { + // 已经是第一章,回到开头 + await seekInBook(Duration.zero); + } + } + Future seek(Duration position, {int? index}); - Future seekInBook(Duration position); + Future seekInBook(Duration position) async { + if (book == null) return; + // 找到目标位置所在音轨和音轨内的位置 + final track = book!.findTrackAtTime(position); + final index = book!.tracks.indexOf(track); + Duration positionInTrack = position - track.startOffset; + if (positionInTrack <= Duration.zero) { + positionInTrack = offset; + } + // 切换到目标音轨具体位置 + await seek(positionInTrack, index: index); + } + Future setSpeed(double speed); Future setVolume(double volume); - Future switchChapter(int chapterId); + Future switchChapter(int chapterId) async { + if (book == null) return; + + final chapter = book!.chapters.firstWhere( + (ch) => ch.id == chapterId, + orElse: () => throw Exception('Chapter not found'), + ); + await seekInBook(chapter.start + offset); + } + + Stream get playingStream; + Stream get bookStream => _bookStreamController.stream; + Stream get chapterStream => _chapterStreamController.stream; int get currentIndex; + double get speed; Duration get position; + Stream get positionStream; Duration get positionInChapter { final globalPosition = positionInBook; @@ -32,10 +157,179 @@ abstract class AbsAudioPlayer { Duration get positionInBook => position + (book?.tracks[currentIndex].startOffset ?? Duration.zero); - Stream get positionStream; + Stream get positionInChapterStream => + positionStream.map((position) { + return positionInChapter; + }); - Stream get positionInChapterStream; + Stream get positionInBookStream => positionStream.map((position) { + return positionInBook; + }); - Stream get positionInBookStream; - Stream get bufferedPositionInBookStream; + Duration get bufferedPosition; + Stream get bufferedPositionStream; + Duration get bufferedPositionInBook => + bufferedPosition + + (book?.tracks[currentIndex].startOffset ?? Duration.zero); + Stream get bufferedPositionInBookStream => + bufferedPositionStream.map((position) { + return bufferedPositionInBook; + }); + + dispose() { + _mediaItemController.close(); + playerStateSubject.close(); + _bookStreamController.close(); + _chapterStreamController.close(); + } +} + +/// Enumerates the different processing states of a player. +enum ProcessingState { + /// 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 PlayerState { + /// Whether the player will play when [processingState] is + /// [ProcessingState.ready]. + final bool playing; + + /// The current processing state of the player. + final ProcessingState processingState; + + PlayerState(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; + + PlayerState copyWith({ + bool? playing, + ProcessingState? processingState, + }) { + return PlayerState( + playing ?? this.playing, + processingState ?? this.processingState, + ); + } +} + +Uri _getUri( + 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'); +} + +/// Backwards compatible extensions on rxdart's ValueStream +extension _ValueStreamExtension on ValueStream { + /// Backwards compatible version of valueOrNull. + T? get nvalue => hasValue ? value : null; +} + +extension FormatNotificationTitle on String { + String formatNotificationTitle(BookExpanded book) { + return replaceAllMapped( + RegExp(r'\$(\w+)'), + (match) { + final type = match.group(1); + return NotificationTitleType.values + .firstWhere((element) => element.name == type) + .extractFrom(book) ?? + match.group(0) ?? + ''; + }, + ); + } +} + +extension NotificationTitleUtils on NotificationTitleType { + String? extractFrom(BookExpanded book) { + var bookMetadataExpanded = book.metadata.asBookMetadataExpanded; + switch (this) { + case NotificationTitleType.bookTitle: + return bookMetadataExpanded.title; + case NotificationTitleType.chapterTitle: + // TODO: implement chapter title; depends on https://github.com/Dr-Blank/Vaani/issues/2 + return bookMetadataExpanded.title; + case NotificationTitleType.author: + return bookMetadataExpanded.authorName; + case NotificationTitleType.narrator: + return bookMetadataExpanded.narratorName; + case NotificationTitleType.series: + return bookMetadataExpanded.seriesName; + case NotificationTitleType.subtitle: + return bookMetadataExpanded.subtitle; + case NotificationTitleType.year: + return bookMetadataExpanded.publishedYear; + } + } +} + +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; + } } diff --git a/lib/shared/audio_player_mpv.dart b/lib/shared/audio_player_mpv.dart new file mode 100644 index 0000000..e19418c --- /dev/null +++ b/lib/shared/audio_player_mpv.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'package:media_kit/media_kit.dart' hide PlayerState; +import 'package:vaani/shared/audio_player.dart'; + +class AbsMpvAudioPlayer extends AbsAudioPlayer { + final player = Player(); + AbsMpvAudioPlayer() { + player.stream.playing.listen((playing) { + final state = playerState; + playerStateSubject.add( + state.copyWith( + playing: playing, + processingState: playing + ? state.processingState == ProcessingState.idle + ? ProcessingState.ready + : state.processingState + : player.state.buffering + ? ProcessingState.buffering + : player.state.completed + ? ProcessingState.completed + : ProcessingState.ready, + ), + ); + }); + } + @override + Stream get bufferedPositionInBookStream => 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 + Duration get position => player.state.position; + + @override + Stream get positionStream => player.stream.position; + + @override + Future seek(Duration position, {int? index}) async { + if (index != null) { + await player.jump(index); + } + await player.seek(position); + } + + @override + Future setPlayList( + List playlist, { + int? index, + Duration? position, + }) async { + await player.open( + Playlist( + playlist.map((uri) => Media(uri.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); + } + + @override + Stream get playingStream => player.stream.playing; + + @override + // TODO: implement speed + double get speed => player.state.rate; + + @override + // TODO: implement bufferedPosition + Duration get bufferedPosition => player.state.buffer; + + @override + // TODO: implement bufferedPositionStream + Stream get bufferedPositionStream => player.stream.buffer; +} diff --git a/lib/shared/widgets/drawer.dart b/lib/shared/widgets/drawer.dart index 10f4dfb..832b379 100644 --- a/lib/shared/widgets/drawer.dart +++ b/lib/shared/widgets/drawer.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:vaani/features/you/view/server_manager.dart'; import 'package:vaani/router/router.dart'; @@ -26,12 +25,12 @@ class MyDrawer extends StatelessWidget { ListTile( title: const Text('server Settings'), onTap: () { - Navigator.of(context).push( - platformPageRoute( - context: context, - builder: (context) => const ServerManagerPage(), - ), - ); + // Navigator.of(context).push( + // PageRoute( + // context: context, + // builder: (context) => const ServerManagerPage(), + // ), + // ); }, ), ListTile( diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 52d3680..1c1160c 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -12,12 +12,9 @@ import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_actions.dart'; import 'package:vaani/features/player/providers/abs_provider.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; -import 'package:vaani/features/player/providers/player_status_provider.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/router/models/library_item_extras.dart'; import 'package:vaani/router/router.dart'; -import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/home_shelf.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; @@ -215,11 +212,8 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final me = ref.watch(meProvider); - // final player = ref.watch(audiobookPlayerProvider); - final currentBook = ref.watch(absStateProvider.select((v) => v.book)); - final playing = ref.watch(absStateProvider.select((v) => v.playing)); - // final playerStatus = ref.watch(playerStatusProvider); - // final isLoading = playerStatus.isLoading(libraryItemId); + final currentBook = ref.watch(currentBookProvider); + final playing = ref.watch(playerStateProvider.select((v) => v.playing)); final isCurrentBookSetInPlayer = currentBook?.libraryItemId == libraryItemId; final isPlayingThisBook = playing && isCurrentBookSetInPlayer; @@ -300,9 +294,9 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { // book.media.asBookExpanded, // userProgress?.currentTime, // ); - ref.read(absStateProvider.notifier).load( + ref.read(absAudioPlayerProvider.notifier).load( book.media.asBookExpanded, - userProgress?.currentTime, + initialPosition: userProgress?.currentTime, ); }, icon: Hero( @@ -355,7 +349,7 @@ class BookCoverWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentBook = ref.watch(absStateProvider.select((v) => v.book)); + final currentBook = ref.watch(currentBookProvider); if (currentBook == null) { return const BookCoverSkeleton(); } diff --git a/lib/shared/widgets/tray_manager.dart b/lib/shared/widgets/tray_manager.dart index 3c79e9f..aa9bcf0 100644 --- a/lib/shared/widgets/tray_manager.dart +++ b/lib/shared/widgets/tray_manager.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:tray_manager/tray_manager.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/shared/utils/helper.dart'; import 'package:window_manager/window_manager.dart'; @@ -46,17 +46,17 @@ class _TrayManagerState extends ConsumerState MenuItem( key: 'play_pause', label: '播放/暂停', - onClick: (menuItem) => ref.read(playerProvider).togglePlayPause(), + onClick: (menuItem) => ref.read(absAudioPlayerProvider).playOrPause(), ), MenuItem( key: 'previous', label: '上一个', - onClick: (menuItem) => ref.read(playerProvider).skipToPrevious(), + onClick: (menuItem) => ref.read(absAudioPlayerProvider).previous(), ), MenuItem( key: 'next', label: '下一个', - onClick: (menuItem) => ref.read(playerProvider).skipToNext(), + onClick: (menuItem) => ref.read(absAudioPlayerProvider).next(), ), MenuItem.separator(), MenuItem( diff --git a/pubspec.lock b/pubspec.lock index c360986..7ff954c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -528,14 +528,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_platform_widgets: - dependency: "direct main" - description: - name: flutter_platform_widgets - sha256: "22a86564cb6cc0b93637c813ca91b0b1f61c2681a31e0f9d77590c1fa9f12020" - url: "https://pub.dev" - source: hosted - version: "9.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index de1a5e4..78682f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: coast: ^2.0.2 collection: ^1.18.0 cupertino_icons: ^1.0.6 - flutter_platform_widgets: ^9.0.0 + # flutter_platform_widgets: ^9.0.0 flutter_staggered_grid_view: ^0.7.0 device_info_plus: ^11.3.3 duration_picker: ^1.2.0