From 114c9761fdee824b053ab2d025ef07fbf5ebc399 Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Sat, 22 Nov 2025 15:54:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=B0=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/constants/sizes.dart | 1 + .../view/library_item_actions.dart | 24 +- .../core/playback_reporter_session.dart | 74 +++--- .../player/core/audiobook_player_session.dart | 127 +++++---- lib/features/player/core/player_status.dart | 58 ++++ .../providers/player_status_provider.dart | 40 +++ .../providers/player_status_provider.g.dart | 25 ++ .../player/providers/session_provider.dart | 180 ++++++------- .../player/providers/session_provider.g.dart | 251 +++--------------- lib/features/player/view/player_expanded.dart | 20 +- .../player/view/player_minimized.dart | 15 +- .../widgets/audiobook_player_seek_button.dart | 4 +- .../audiobook_player_seek_chapter_button.dart | 51 +--- .../widgets/chapter_selection_button.dart | 30 +-- .../widgets/player_player_pause_button.dart | 76 +++--- .../view/widgets/player_progress_bar.dart | 27 +- .../player_skip_chapter_start_end.dart | 20 +- .../providers/shake_detector.dart | 19 +- .../providers/shake_detector.g.dart | 2 +- .../skip_start_end/skip_start_end.dart | 117 ++------ .../skip_start_end_provider.dart | 40 ++- .../skip_start_end_provider.g.dart | 2 +- lib/framework.dart | 25 +- lib/generated/intl/messages_en.dart | 12 + lib/generated/intl/messages_zh.dart | 6 + lib/generated/l10n.dart | 55 ++++ lib/l10n/intl_en.arb | 7 + lib/l10n/intl_zh.arb | 7 + lib/router/scaffold_with_nav_bar.dart | 6 +- lib/shared/widgets/shelves/book_shelf.dart | 20 +- 30 files changed, 658 insertions(+), 683 deletions(-) create mode 100644 lib/features/player/core/player_status.dart create mode 100644 lib/features/player/providers/player_status_provider.dart create mode 100644 lib/features/player/providers/player_status_provider.g.dart rename lib/features/{skip_start_end => player/view/widgets}/player_skip_chapter_start_end.dart (81%) diff --git a/lib/constants/sizes.dart b/lib/constants/sizes.dart index b8f3c20..dbf64c1 100644 --- a/lib/constants/sizes.dart +++ b/lib/constants/sizes.dart @@ -14,4 +14,5 @@ class AppElementSizes { static const double iconSizeLarge = 64.0; static const double barHeight = 3.0; + static const double barHeightLarge = 5.0; } diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index a97cb1a..2234091 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -15,6 +15,7 @@ import 'package:vaani/features/downloads/providers/download_manager.dart' itemDownloadProgressProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/player/providers/player_form.dart'; +import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; @@ -299,7 +300,7 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isBookPlaying = ref.watch(playStateProvider).playing; + final isBookPlaying = ref.watch(sessionProvider)?.libraryItemId == item.id; return IconButton( onPressed: () { @@ -431,15 +432,14 @@ class _LibraryItemPlayButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(sessionProvider); final book = item.media.asBookExpanded; - final session = ref.watch(sessionProvider.select((v) => v.session)); - final sessionLoading = - ref.watch(sessionLoadingProvider(book.libraryItemId)); - final playerState = ref.watch(playStateProvider); - // final player = ref.watch(audiobookPlayerProvider); + final playerStatusNotifier = ref.watch(playerStatusProvider); + final isLoading = playerStatusNotifier.isLoading(book.libraryItemId); final isCurrentBookSetInPlayer = session?.libraryItemId == book.libraryItemId; - final isPlayingThisBook = playerState.playing && isCurrentBookSetInPlayer; + final isPlayingThisBook = + playerStatusNotifier.isPlaying() && isCurrentBookSetInPlayer; final userMediaProgress = item.userMediaProgress; final isBookCompleted = userMediaProgress?.isFinished ?? false; @@ -466,13 +466,15 @@ class _LibraryItemPlayButton extends HookConsumerWidget { } return ElevatedButton.icon( - onPressed: () => session?.libraryItemId == book.libraryItemId - ? ref.read(sessionProvider).load(book.libraryItemId, null) - : ref.read(playerProvider).togglePlayPause(), + onPressed: () { + session?.libraryItemId == book.libraryItemId + ? ref.read(playerProvider).togglePlayPause() + : ref.read(sessionProvider.notifier).load(book.libraryItemId, null); + }, icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + book.libraryItemId, child: DynamicItemPlayIcon( - isLoading: sessionLoading, + isLoading: isLoading, isCurrentBookSetInPlayer: isCurrentBookSetInPlayer, isPlayingThisBook: isPlayingThisBook, isBookCompleted: isBookCompleted, diff --git a/lib/features/playback_reporting/core/playback_reporter_session.dart b/lib/features/playback_reporting/core/playback_reporter_session.dart index e002d28..1e53dae 100644 --- a/lib/features/playback_reporting/core/playback_reporter_session.dart +++ b/lib/features/playback_reporting/core/playback_reporter_session.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart'; +import 'package:vaani/features/player/core/audiobook_player_session.dart'; import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('PlaybackReporter'); @@ -14,7 +14,7 @@ final _logger = Logger('PlaybackReporter'); /// and also report when the player is paused/stopped/finished/playing class PlaybackReporter { /// The player to watch - final AudiobookPlayer player; + final AbsAudioHandler player; /// the api to report to final AudiobookshelfApi authenticatedApi; @@ -55,7 +55,7 @@ class PlaybackReporter { PlaybackReporter( this.player, this.authenticatedApi, { - required PlaybackSession session, + required PlaybackSessionExpanded session, this.reportingDurationThreshold = const Duration(seconds: 1), Duration reportingInterval = const Duration(seconds: 10), this.minimumPositionForReporting, @@ -63,28 +63,28 @@ class PlaybackReporter { }) : _reportingInterval = reportingInterval, _session = session { // initial conditions - if (player.playing) { - _stopwatch.start(); - _setReportTimerIfNotAlready(); - _logger.fine('starting stopwatch'); - } else { - _logger.fine('not starting stopwatch'); - } + // if (player.playing) { + // _stopwatch.start(); + // _setReportTimerIfNotAlready(); + // _logger.fine('starting stopwatch'); + // } else { + // _logger.fine('not starting stopwatch'); + // } _subscriptions.add( player.playerStateStream.listen((state) async { // set timer if any book is playing and cancel if not - if (player.book != null) { - if (state.playing) { - _setReportTimerIfNotAlready(); - } else { - _cancelReportTimer(); - } - } else if (player.book == null && _reportTimer != null) { - _logger.info('book is null, closing session'); - await closeSession(); + // if (player.book != null) { + if (state.playing) { + _setReportTimerIfNotAlready(); + } else { _cancelReportTimer(); } + // } else if (player.book == null && _reportTimer != null) { + // _logger.info('book is null, closing session'); + // await closeSession(); + // _cancelReportTimer(); + // } // start or stop the stopwatch based on the playing state if (state.playing) { @@ -114,9 +114,7 @@ class PlaybackReporter { _logger.fine( 'callback called when elapsed ${_stopwatch.elapsed}', ); - if (player.book != null && - player.positionInBook >= - player.book!.duration - markCompleteWhenTimeLeft) { + if (player.positionInBook >= _session.duration - markCompleteWhenTimeLeft) { _logger.info( 'marking complete as time left is less than $markCompleteWhenTimeLeft', ); @@ -145,23 +143,23 @@ class PlaybackReporter { /// current sessionId /// this is used to report the playback - PlaybackSession? _session; - String? get sessionId => _session?.id; + PlaybackSession _session; + String? get sessionId => _session.id; Future markComplete() async { - if (player.book == null) { - throw NoAudiobookPlayingError(); - } + // if (player.book == null) { + // throw NoAudiobookPlayingError(); + // } await authenticatedApi.me.createUpdateMediaProgress( - libraryItemId: player.book!.libraryItemId, + libraryItemId: _session.libraryItemId, parameters: CreateUpdateProgressReqParams( isFinished: true, currentTime: player.positionInBook, - duration: player.book!.duration, + duration: _session.duration, ), responseErrorHandler: _responseErrorHandler, ); - _logger.info('Marked complete for book: ${player.book!.libraryItemId}'); + _logger.info('Marked complete for book: ${_session.libraryItemId}'); } Future syncCurrentPosition() async { @@ -197,7 +195,7 @@ class PlaybackReporter { parameters: _getSyncData(), responseErrorHandler: _responseErrorHandler, ); - _session = null; + // _session = null; _logger.info('Closed session'); } @@ -223,12 +221,12 @@ class PlaybackReporter { } SyncSessionReqParams? _getSyncData() { - if (player.book?.libraryItemId != _session?.libraryItemId) { - _logger.info( - 'Book changed, not syncing position for session: $sessionId', - ); - return null; - } + // if (player.book?.libraryItemId != _session?.libraryItemId) { + // _logger.info( + // 'Book changed, not syncing position for session: $sessionId', + // ); + // return null; + // } // if in the ignore duration, don't sync if (minimumPositionForReporting != null && @@ -249,7 +247,7 @@ class PlaybackReporter { return SyncSessionReqParams( currentTime: player.positionInBook, timeListened: _stopwatch.elapsed, - duration: player.book?.duration ?? Duration.zero, + duration: _session.duration, ); } } diff --git a/lib/features/player/core/audiobook_player_session.dart b/lib/features/player/core/audiobook_player_session.dart index 0485657..37c2d8f 100644 --- a/lib/features/player/core/audiobook_player_session.dart +++ b/lib/features/player/core/audiobook_player_session.dart @@ -6,8 +6,10 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/providers/session_provider.dart'; +import 'package:vaani/features/player/core/player_status.dart' as core; +import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/shared/extensions/chapter.dart'; // add a small offset so the display does not show the previous chapter for a split second @@ -20,27 +22,28 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { PlaybackSessionExpanded? _session; + final _currentChapterObject = BehaviorSubject.seeded(null); AbsAudioHandler(this.ref) { _setupAudioPlayer(); } void _setupAudioPlayer() { - // // 监听播放位置变化,更新全局位置 - // _player.positionStream.listen((position) { - // // _updateGlobalPosition(position); - // }); - - // // 监听音轨变化 - // _player.currentIndexStream.listen((index) { - // if (index != null) { - // _onTrackChanged(index); - // } - // }); + final statusNotifier = ref.read(playerStatusProvider.notifier); // 转发播放状态 _player.playbackEventStream.map(_transformEvent).pipe(playbackState); - _player.playerStateStream.distinct().listen((event) { - ref.read(playStateProvider.notifier).setState(event); + _player.playerStateStream.listen((event) { + if (event.playing) { + statusNotifier.setPlayStatusVerify(core.PlayStatus.playing); + } else { + statusNotifier.setPlayStatusVerify(core.PlayStatus.paused); + } + }); + _player.positionStream.distinct().listen((position) { + final chapter = _session?.findChapterAtTime(positionInBook); + if (chapter != currentChapter) { + _currentChapterObject.sink.add(chapter); + } }); } @@ -109,58 +112,85 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { (ch) => ch.id == chapterId, orElse: () => throw Exception('Chapter not found'), ); - await seekInBook(chapter.start + offset); } - Duration get positionInBook { - if (_session != null && _player.currentIndex != null) { - return _session!.audioTracks[_player.currentIndex!].startOffset + - _player.position; - } - return Duration.zero; - } + PlaybackSessionExpanded? get session => _session; // 当前音轨 AudioTrack? get currentTrack { - if (_session == null) { + if (_session == null || _player.currentIndex == null) { return null; } - return _session!.findTrackAtTime(positionInBook); + return _session!.audioTracks[_player.currentIndex!]; } // 当前章节 BookChapter? get currentChapter { - if (_session == null) { - return null; - } - return _session!.findChapterAtTime(positionInBook); + return _currentChapterObject.value; + } + + Duration get position => _player.position; + Duration get positionInChapter { + return _player.position + + (currentTrack?.startOffset ?? Duration.zero) - + (currentChapter?.start ?? Duration.zero); + } + + Duration get positionInBook { + return _player.position + (currentTrack?.startOffset ?? Duration.zero); + } + + Duration get bufferedPositionInBook { + return _player.bufferedPosition + + (currentTrack?.startOffset ?? Duration.zero); } Duration? get chapterDuration => currentChapter?.duration; + + Stream get playerStateStream => _player.playerStateStream; + Stream get positionStream => _player.positionStream; - Stream get positionStreamInChapter { + + Stream get positionStreamInBook { return _player.positionStream.map((position) { - final currentIndex = _player.currentIndex; - if (_session == null || currentIndex == null) { - return Duration.zero; - } - final globalPosition = - position + _session!.audioTracks[currentIndex].startOffset; - final chapter = _session!.findChapterAtTime(globalPosition); - return globalPosition - chapter.start; + return position + (currentTrack?.startOffset ?? Duration.zero); }); } - Future togglePlayPause() { + Stream get slowPositionStreamInBook { + final superPositionStream = _player.createPositionStream( + steps: 100, + minPeriod: const Duration(milliseconds: 500), + maxPeriod: const Duration(seconds: 1), + ); + return superPositionStream.map((position) { + return position + (currentTrack?.startOffset ?? Duration.zero); + }); + } + + Stream get bufferedPositionStreamInBook { + return _player.bufferedPositionStream.map((position) { + return position + (currentTrack?.startOffset ?? Duration.zero); + }); + } + + Stream get positionStreamInChapter { + return _player.positionStream.distinct().map((position) { + return position + + (currentTrack?.startOffset ?? Duration.zero) - + (currentChapter?.start ?? Duration.zero); + }); + } + + Stream get chapterStream => _currentChapterObject.stream; + + Future togglePlayPause() async { // check if book is set if (_session == null) { return Future.value(); } - - return switch (_player.playerState) { - PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), - }; + _player.playerState.playing ? await pause() : await play(); } // 播放控制方法 @@ -196,12 +226,8 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { @override Future skipToPrevious() async { - if (_session == null) { - return _player.seekToPrevious(); - } - final chapter = currentChapter; - if (chapter == null) { + if (_session == null || chapter == null) { return _player.seekToPrevious(); } final currentIndex = _session!.chapters.indexOf(chapter); @@ -243,8 +269,8 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final track = _session!.findTrackAtTime(globalPosition); final index = _session!.audioTracks.indexOf(track); Duration positionInTrack = globalPosition - track.startOffset; - if (positionInTrack <= Duration.zero) { - positionInTrack = offset; + if (positionInTrack < Duration.zero) { + positionInTrack = Duration.zero; } // 切换到目标音轨具体位置 await _player.seek(positionInTrack, index: index); @@ -264,6 +290,7 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { systemActions: { if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious, MediaAction.rewind, + MediaAction.seek, MediaAction.fastForward, MediaAction.stop, MediaAction.setSpeed, @@ -280,7 +307,7 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { AudioProcessingState.idle, playing: _player.playing, updatePosition: _player.position, - bufferedPosition: _player.bufferedPosition, + bufferedPosition: event.bufferedPosition, speed: _player.speed, queueIndex: event.currentIndex, captioningEnabled: false, diff --git a/lib/features/player/core/player_status.dart b/lib/features/player/core/player_status.dart new file mode 100644 index 0000000..e911cff --- /dev/null +++ b/lib/features/player/core/player_status.dart @@ -0,0 +1,58 @@ +enum PlayStatus { stopped, playing, paused, hidden, loading, completed } + +class PlayerStatus { + PlayStatus playStatus; + String itemId; + bool quite; + + PlayerStatus({ + this.playStatus = PlayStatus.hidden, + this.itemId = '', + this.quite = false, + }) { + // addListener(_onStatusChanged); + } + bool isPlaying({String? itemId}) { + if (itemId != null && this.itemId.isNotEmpty) { + return playStatus == PlayStatus.playing && this.itemId == itemId; + } else { + return playStatus == PlayStatus.playing; + } + } + + bool isPaused({String? itemId}) { + if (itemId != null && this.itemId.isNotEmpty) { + return playStatus == PlayStatus.paused && this.itemId == itemId; + } else { + return playStatus == PlayStatus.paused; + } + } + + bool isStopped({String? itemId}) { + if (itemId != null && this.itemId.isNotEmpty) { + return playStatus == PlayStatus.stopped && this.itemId == itemId; + } else { + return playStatus == PlayStatus.stopped; + } + } + + bool isLoading(String? itemId) { + if (itemId != null && this.itemId.isNotEmpty) { + return playStatus == PlayStatus.loading && this.itemId == itemId; + } else { + return playStatus == PlayStatus.loading; + } + } + + PlayerStatus copyWith({ + PlayStatus? playStatus, + String? itemId, + bool? quite, + }) { + return PlayerStatus( + playStatus: playStatus ?? this.playStatus, + itemId: itemId ?? this.itemId, + quite: quite ?? this.quite, + ); + } +} diff --git a/lib/features/player/providers/player_status_provider.dart b/lib/features/player/providers/player_status_provider.dart new file mode 100644 index 0000000..2c24d6e --- /dev/null +++ b/lib/features/player/providers/player_status_provider.dart @@ -0,0 +1,40 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:vaani/features/player/core/player_status.dart' as core; + +part 'player_status_provider.g.dart'; + +@Riverpod(keepAlive: true) +class PlayerStatus extends _$PlayerStatus { + @override + core.PlayerStatus build() { + return core.PlayerStatus(); + } + + void setPlayStatus(core.PlayStatus playStatus) { + state = state.copyWith(playStatus: playStatus); + } + + void setPlayStatusQuietly(core.PlayStatus playStatus) { + // state.copyWith(quite: true); + setPlayStatus(playStatus); + // state.copyWith(quite: false); + } + + // 校验原值, 不相同则更新 + void setPlayStatusVerify(core.PlayStatus playStatus) { + if (state.playStatus != playStatus) { + setPlayStatus(playStatus); + } + } + + void setLoading(String itemId) { + state = state.copyWith( + playStatus: core.PlayStatus.loading, + itemId: itemId, + ); + } + + void setHidden() { + state = state.copyWith(playStatus: core.PlayStatus.hidden); + } +} diff --git a/lib/features/player/providers/player_status_provider.g.dart b/lib/features/player/providers/player_status_provider.g.dart new file mode 100644 index 0000000..5991a44 --- /dev/null +++ b/lib/features/player/providers/player_status_provider.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'player_status_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$playerStatusHash() => r'4a8f222b8c1d5c92883f4358c69571c35a378861'; + +/// See also [PlayerStatus]. +@ProviderFor(PlayerStatus) +final playerStatusProvider = + NotifierProvider.internal( + PlayerStatus.new, + name: r'playerStatusProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playerStatusHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PlayerStatus = Notifier; +// 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/session_provider.dart b/lib/features/player/providers/session_provider.dart index fb218bd..44e9d08 100644 --- a/lib/features/player/providers/session_provider.dart +++ b/lib/features/player/providers/session_provider.dart @@ -1,6 +1,5 @@ import 'package:audio_service/audio_service.dart'; import 'package:http/http.dart' as http; -import 'package:just_audio/just_audio.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -9,24 +8,55 @@ 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/playback_reporting/core/playback_reporter_session.dart' + as core; import 'package:vaani/features/player/core/audiobook_player_session.dart'; +import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/obfuscation.dart'; part 'session_provider.g.dart'; -class SessionPlayer { - late final AbsAudioHandler _audioService; - core.PlaybackSessionExpanded? _session; - Ref ref; - SessionPlayer(this.ref); - void setAudioService(AbsAudioHandler audioPlayer) { - _audioService = audioPlayer; +@Riverpod(keepAlive: true) +Future audioHandlerInit(Ref ref) async { + // JustAudioMediaKit.ensureInitialized(windows: false); + JustAudioMediaKit.ensureInitialized(); + final audioService = await AudioService.init( + builder: () => AbsAudioHandler(ref), + config: const AudioServiceConfig( + androidNotificationChannelId: 'com.vaani.rang.channel.audio', + androidNotificationChannelName: 'ABSPlayback', + androidNotificationChannelDescription: + 'Needed to control audio from lock screen', + androidNotificationOngoing: false, + androidStopForegroundOnPause: false, + androidNotificationIcon: 'drawable/ic_stat_logo', + preloadArtwork: true, + ), + ); + return audioService; +} + +@Riverpod(keepAlive: true) +class Player extends _$Player { + @override + AbsAudioHandler build() { + return ref.watch(audioHandlerInitProvider).requireValue; + } +} + +@Riverpod(keepAlive: true) +class Session extends _$Session { + @override + core.PlaybackSessionExpanded? build() { + return null; } Future load(String id, String? episodeId) async { - ref.read(sessionLoadingProvider(id).notifier).setLoading(); + final audioService = ref.read(playerProvider); + await audioService.pause(); + ref.read(playerStatusProvider.notifier).setLoading(id); final api = ref.read(authenticatedApiProvider); final playBack = await api.items.play( libraryItemId: id, @@ -52,7 +82,7 @@ class SessionPlayer { ), responseErrorHandler: _responseErrorHandler, ) as core.PlaybackSessionExpanded; - + state = playBack; final downloadManager = ref.read(simpleDownloadManagerProvider); final libItem = await ref.read(libraryItemProvider(playBack.libraryItemId).future); @@ -66,35 +96,29 @@ class SessionPlayer { appPlayerSettings.configurePlayerForEveryBook; await Future.wait([ - _audioService.setSourceAudiobook( + audioService.setSourceAudiobook( playBack, baseUrl: api.baseUrl, token: api.token!, downloadedUris: downloadedUris, ), // set the volume - _audioService.setVolume( + audioService.setVolume( configurePlayerForEveryBook ? bookPlayerSettings.preferredDefaultVolume ?? appPlayerSettings.preferredDefaultVolume : appPlayerSettings.preferredDefaultVolume, ), // set the speed - _audioService.setSpeed( + audioService.setSpeed( configurePlayerForEveryBook ? bookPlayerSettings.preferredDefaultSpeed ?? appPlayerSettings.preferredDefaultSpeed : appPlayerSettings.preferredDefaultSpeed, ), ]); - _session = playBack; - ref.read(sessionLoadingProvider(id).notifier).setLoaded(); - ref.notifyListeners(); } - AbsAudioHandler get audioService => _audioService; - core.PlaybackSession? get session => _session; - void _responseErrorHandler(http.Response response, [error]) { if (response.statusCode != 200) { appLogger.severe('Error with api: ${response.obfuscate()}, $error'); @@ -106,104 +130,48 @@ class SessionPlayer { } @Riverpod(keepAlive: true) -class Player extends _$Player { +class CurrentChapter extends _$CurrentChapter { @override - AbsAudioHandler build() { - final audioService = ref.watch(sessionProvider).audioService; - // audioService.positionStream.listen((position){ - - // }); - return audioService; + core.BookChapter? build() { + final player = ref.watch(playerProvider); + player.chapterStream.distinct().listen((chapter) { + update(chapter); + }); + return player.currentChapter; } - Future togglePlayPause() => state.togglePlayPause(); - Future play() => state.play(); - Future pause() => state.pause(); - Future seekInBook(Duration globalPosition) => - state.seekInBook(globalPosition); + void update(core.BookChapter? chapter) { + if (state != chapter) { + state = chapter; + } + } } @Riverpod(keepAlive: true) -SessionPlayer session(Ref ref) { - return SessionPlayer(ref); -} - -@Riverpod(keepAlive: true) -class SessionLoading extends _$SessionLoading { +class PlaybackReporter extends _$PlaybackReporter { @override - bool build(String itemId) { - return false; - } + Future build() async { + final session = ref.watch(sessionProvider); + if (session == null) { + return null; + } + final playerSettings = ref.watch(appSettingsProvider).playerSettings; + final player = ref.watch(playerProvider); + final api = ref.watch(authenticatedApiProvider); - setLoading() { - state = true; - } - - setLoaded() { - state = false; + final reporter = core.PlaybackReporter( + player, + api, + reportingInterval: playerSettings.playbackReportInterval, + markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, + minimumPositionForReporting: playerSettings.minimumPositionForReporting, + session: session, + ); + ref.onDispose(reporter.dispose); + return reporter; } } -@Riverpod(keepAlive: true) -class PlayState extends _$PlayState { - @override - PlayerState build() { - return PlayerState(false, ProcessingState.idle); - } - - void setState(PlayerState playerState) { - state = playerState; - } -} - -@riverpod -core.BookChapter? currentChapter(Ref ref) { - return ref.watch(playerProvider.select((v) => v.currentChapter)); -} - -@Riverpod(keepAlive: true) -Future audioHandlerInit(Ref ref) async { - // JustAudioMediaKit.ensureInitialized(windows: false); - JustAudioMediaKit.ensureInitialized(); - final audioService = await AudioService.init( - builder: () => AbsAudioHandler(ref), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.vaani.rang.channel.audio', - androidNotificationChannelName: 'ABSPlayback', - androidNotificationChannelDescription: - 'Needed to control audio from lock screen', - androidNotificationOngoing: false, - androidStopForegroundOnPause: false, - androidNotificationIcon: 'drawable/ic_stat_logo', - preloadArtwork: true, - ), - ); - ref.read(sessionProvider).setAudioService(audioService); - return audioService; -} - -// @Riverpod(keepAlive: true) -// class PlaybackReporter extends _$PlaybackReporter { -// @override -// Future build() async { -// final playerSettings = ref.watch(appSettingsProvider).playerSettings; -// final player = ref.watch(playerProvider); -// final session = ref.watch(sessionProvider.select((v) => v.session)); -// final api = ref.watch(authenticatedApiProvider); - -// final reporter = core.PlaybackReporter( -// player.player, -// api, -// reportingInterval: playerSettings.playbackReportInterval, -// markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, -// minimumPositionForReporting: playerSettings.minimumPositionForReporting, -// session: session, -// ); -// ref.onDispose(reporter.dispose); -// return reporter; -// } -// } - class PlaybackSyncError implements Exception { String message; diff --git a/lib/features/player/providers/session_provider.g.dart b/lib/features/player/providers/session_provider.g.dart index 4ecebde..a14dfd5 100644 --- a/lib/features/player/providers/session_provider.g.dart +++ b/lib/features/player/providers/session_provider.g.dart @@ -6,40 +6,7 @@ part of 'session_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$sessionHash() => r'ae97659a7772abaa3c97644f39af6b3f05c75faf'; - -/// See also [session]. -@ProviderFor(session) -final sessionProvider = Provider.internal( - session, - name: r'sessionProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef SessionRef = ProviderRef; -String _$currentChapterHash() => r'a2f43d62f77ce48e6ca34c89700443f67dbd78fe'; - -/// See also [currentChapter]. -@ProviderFor(currentChapter) -final currentChapterProvider = AutoDisposeProvider.internal( - currentChapter, - name: r'currentChapterProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentChapterHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef CurrentChapterRef = AutoDisposeProviderRef; -String _$audioHandlerInitHash() => r'64bc78439049068ec6de6e19af657d410bde9581'; +String _$audioHandlerInitHash() => r'5677b2267f472b667ce7a63cc5c91c4320d630e8'; /// See also [audioHandlerInit]. @ProviderFor(audioHandlerInit) @@ -56,7 +23,7 @@ final audioHandlerInitProvider = FutureProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef AudioHandlerInitRef = FutureProviderRef; -String _$playerHash() => r'41cc626fd4a3317ce7e1ffa3c5e03206a9819231'; +String _$playerHash() => r'9599b094cdd9eca614c27ec5bdf2d5259d20ac5f'; /// See also [Player]. @ProviderFor(Player) @@ -70,184 +37,52 @@ final playerProvider = NotifierProvider.internal( ); typedef _$Player = Notifier; -String _$sessionLoadingHash() => r'4688469dd8ac9f38063917ede032cfe1506a63a8'; +String _$sessionHash() => r'ce9405cc0b8247014924a005fa60fbc3bf92d5c6'; -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$SessionLoading extends BuildlessNotifier { - late final String itemId; - - bool build( - String itemId, - ); -} - -/// See also [SessionLoading]. -@ProviderFor(SessionLoading) -const sessionLoadingProvider = SessionLoadingFamily(); - -/// See also [SessionLoading]. -class SessionLoadingFamily extends Family { - /// See also [SessionLoading]. - const SessionLoadingFamily(); - - /// See also [SessionLoading]. - SessionLoadingProvider call( - String itemId, - ) { - return SessionLoadingProvider( - itemId, - ); - } - - @override - SessionLoadingProvider getProviderOverride( - covariant SessionLoadingProvider provider, - ) { - return call( - provider.itemId, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'sessionLoadingProvider'; -} - -/// See also [SessionLoading]. -class SessionLoadingProvider - extends NotifierProviderImpl { - /// See also [SessionLoading]. - SessionLoadingProvider( - String itemId, - ) : this._internal( - () => SessionLoading()..itemId = itemId, - from: sessionLoadingProvider, - name: r'sessionLoadingProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$sessionLoadingHash, - dependencies: SessionLoadingFamily._dependencies, - allTransitiveDependencies: - SessionLoadingFamily._allTransitiveDependencies, - itemId: itemId, - ); - - SessionLoadingProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.itemId, - }) : super.internal(); - - final String itemId; - - @override - bool runNotifierBuild( - covariant SessionLoading notifier, - ) { - return notifier.build( - itemId, - ); - } - - @override - Override overrideWith(SessionLoading Function() create) { - return ProviderOverride( - origin: this, - override: SessionLoadingProvider._internal( - () => create()..itemId = itemId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - itemId: itemId, - ), - ); - } - - @override - NotifierProviderElement createElement() { - return _SessionLoadingProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is SessionLoadingProvider && other.itemId == itemId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, itemId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin SessionLoadingRef on NotifierProviderRef { - /// The parameter `itemId` of this provider. - String get itemId; -} - -class _SessionLoadingProviderElement - extends NotifierProviderElement - with SessionLoadingRef { - _SessionLoadingProviderElement(super.provider); - - @override - String get itemId => (origin as SessionLoadingProvider).itemId; -} - -String _$playStateHash() => r'5256c4154c4254e406593035bc54d917a9a059bf'; - -/// See also [PlayState]. -@ProviderFor(PlayState) -final playStateProvider = NotifierProvider.internal( - PlayState.new, - name: r'playStateProvider', +/// See also [Session]. +@ProviderFor(Session) +final sessionProvider = + NotifierProvider.internal( + Session.new, + name: r'sessionProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$playStateHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$PlayState = Notifier; +typedef _$Session = Notifier; +String _$currentChapterHash() => r'0be02fbc4f65b30da4ce18964ee457a18c4d1073'; + +/// See also [CurrentChapter]. +@ProviderFor(CurrentChapter) +final currentChapterProvider = + NotifierProvider.internal( + CurrentChapter.new, + name: r'currentChapterProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentChapterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$CurrentChapter = Notifier; +String _$playbackReporterHash() => r'b9a2197a12e83f9760bd61ae2a1ad8dff82042e9'; + +/// See also [PlaybackReporter]. +@ProviderFor(PlaybackReporter) +final playbackReporterProvider = + AsyncNotifierProvider.internal( + PlaybackReporter.new, + name: r'playbackReporterProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$playbackReporterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PlaybackReporter = AsyncNotifier; // 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 2648b30..9775064 100644 --- a/lib/features/player/view/player_expanded.dart +++ b/lib/features/player/view/player_expanded.dart @@ -7,7 +7,7 @@ import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/session_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/player_skip_chapter_start_end.dart'; +import 'package:vaani/features/player/view/widgets/player_skip_chapter_start_end.dart'; import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; @@ -26,7 +26,7 @@ class PlayerExpanded extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider).session; + final session = ref.watch(sessionProvider); if (session == null) { return SizedBox.shrink(); } @@ -148,16 +148,14 @@ class PlayerExpanded extends HookConsumerWidget { ), ), - Expanded( - child: SizedBox( - width: imageSize, - child: Padding( - padding: EdgeInsets.only( - left: AppElementSizes.paddingRegular, - right: AppElementSizes.paddingRegular, - ), - child: const AudiobookProgressBar(), + SizedBox( + width: imageSize, + child: Padding( + padding: EdgeInsets.only( + left: AppElementSizes.paddingRegular, + right: AppElementSizes.paddingRegular, ), + child: const AudiobookProgressBar(), ), ), diff --git a/lib/features/player/view/player_minimized.dart b/lib/features/player/view/player_minimized.dart index 638ea90..2824e26 100644 --- a/lib/features/player/view/player_minimized.dart +++ b/lib/features/player/view/player_minimized.dart @@ -1,5 +1,6 @@ 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'; @@ -16,7 +17,7 @@ class PlayerMinimized extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider).session; + final session = ref.watch(sessionProvider); if (session == null) { return SizedBox.shrink(); } @@ -57,14 +58,14 @@ class PlayerMinimized extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ // AutoScrollText( - Text( + PlatformText( '${session.displayTitle} - ${currentChapter?.title ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, // velocity: // const Velocity(pixelsPerSecond: Offset(16, 0)), style: Theme.of(context).textTheme.bodyLarge, ), - Text( + PlatformText( session.displayAuthor, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -83,7 +84,7 @@ class PlayerMinimized extends HookConsumerWidget { // rewind button Padding( padding: const EdgeInsets.only(left: 8), - child: IconButton( + child: PlatformIconButton( icon: const Icon( Icons.replay_30, size: AppElementSizes.iconSizeSmall, @@ -126,12 +127,10 @@ class PlayerMinimizedFramework extends HookConsumerWidget { SizedBox( height: AppElementSizes.barHeight, child: LinearProgressIndicator( - // value: (progress.data ?? Duration.zero).inSeconds / - // player.book!.duration.inSeconds, value: (progress.data ?? Duration.zero).inSeconds / (player.chapterDuration?.inSeconds ?? 1), - color: Theme.of(context).colorScheme.onPrimaryContainer, - backgroundColor: Theme.of(context).colorScheme.primaryContainer, + // color: Theme.of(context).colorScheme.onPrimaryContainer, + // backgroundColor: Theme.of(context).colorScheme.primaryContainer, ), ), ], 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 4781113..152fa90 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/session_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(audiobookPlayerProvider); + final player = ref.watch(playerProvider); 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 749c66b..282c7ce 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/session_provider.dart'; class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { const AudiobookPlayerSeekChapterButton({ @@ -14,63 +14,26 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); - - // // add a small offset so the display does not show the previous chapter for a split second - // const offset = Duration(milliseconds: 10); - - // /// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter - // const doNotSeekBackIfLessThan = Duration(seconds: 5); - - // /// seek forward to the next chapter - // void seekForward() { - // final index = player.book!.chapters.indexOf(player.currentChapter!); - // if (index < player.book!.chapters.length - 1) { - // player.seek( - // player.book!.chapters[index + 1].start + offset, - // ); - // } else { - // player.seek(player.currentChapter!.end); - // } - // } - - // /// seek backward to the previous chapter or the start of the current chapter - // void seekBackward() { - // final currentPlayingChapterIndex = - // player.book!.chapters.indexOf(player.currentChapter!); - // final chapterPosition = - // player.positionInBook - player.currentChapter!.start; - // BookChapter chapterToSeekTo; - // // if player position is less than 5 seconds into the chapter, go to the previous chapter - // if (chapterPosition < doNotSeekBackIfLessThan && - // currentPlayingChapterIndex > 0) { - // chapterToSeekTo = player.book!.chapters[currentPlayingChapterIndex - 1]; - // } else { - // chapterToSeekTo = player.currentChapter!; - // } - // player.seek( - // chapterToSeekTo.start + offset, - // ); - // } - + final player = ref.watch(playerProvider); return IconButton( icon: Icon( isForward ? Icons.skip_next : Icons.skip_previous, size: AppElementSizes.iconSizeSmall, ), onPressed: () { - if (player.book == null) { + if (player.session == 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); + player + .seekInBook(isForward ? player.session!.duration : Duration.zero); return; } if (isForward) { - player.seekToNext(); + player.skipToNext(); } else { - player.seekToPrevious(); + player.skipToPrevious(); } }, ); diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index 2eb533d..988b020 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,13 +1,11 @@ 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/audiobook_player.dart' - show audiobookPlayerProvider; -import 'package:vaani/features/player/providers/currently_playing_provider.dart' - show currentPlayingChapterProvider, currentlyPlayingBookProvider; +import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart' show pendingPlayerModals; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration; import 'package:vaani/shared/extensions/duration_format.dart' @@ -22,14 +20,14 @@ class ChapterSelectionButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Tooltip( - message: 'Chapters', + message: S.of(context).chapters, child: IconButton( icon: const Icon(Icons.menu_book_rounded), onPressed: () async { pendingPlayerModals++; await showModalBottomSheet( context: context, - barrierLabel: 'Select Chapter', + barrierLabel: S.of(context).chapterSelect, constraints: BoxConstraints( // 40% of the screen height maxHeight: MediaQuery.of(context).size.height * 0.4, @@ -55,9 +53,9 @@ class ChapterSelectionModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentChapter = ref.watch(currentPlayingChapterProvider); - final currentBook = ref.watch(currentlyPlayingBookProvider); - final notifier = ref.watch(audiobookPlayerProvider); + final session = ref.watch(sessionProvider); + final currentChapter = ref.watch(currentChapterProvider); + final currentChapterIndex = currentChapter?.id; final chapterKey = GlobalKey(); scrollToCurrentChapter() async { @@ -77,7 +75,7 @@ class ChapterSelectionModal extends HookConsumerWidget { children: [ ListTile( title: Text( - 'Chapters${currentChapterIndex == null ? '' : ' (${currentChapterIndex + 1}/${currentBook?.chapters.length})'}', + '${S.of(context).chapters} ${currentChapterIndex == null ? '' : ' (${currentChapterIndex + 1}/${session?.chapters.length})'}', ), ), // scroll to current chapter after opening the dialog @@ -85,10 +83,10 @@ class ChapterSelectionModal extends HookConsumerWidget { child: Scrollbar( child: SingleChildScrollView( primary: true, - child: currentBook?.chapters == null - ? const Text('No chapters found') + child: session?.chapters == null + ? Text(S.of(context).chapterNotFound) : Column( - children: currentBook!.chapters.map( + children: session!.chapters.map( (chapter) { final isCurrent = currentChapterIndex == chapter.id; final isPlayed = currentChapterIndex != null && @@ -117,9 +115,9 @@ class ChapterSelectionModal extends HookConsumerWidget { key: isCurrent ? chapterKey : null, onTap: () { Navigator.of(context).pop(); - // notifier.seekInBook(chapter.start + 90.ms); - notifier.skipToChapter(chapter.id); - notifier.play(); + ref + .read(playerProvider) + .skipToChapter(chapter.id); }, ); }, 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 dd928bb..cdc6b02 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:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:vaani/constants/sizes.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/session_provider.dart'; class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { @@ -14,42 +14,42 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { final double iconSize; @override Widget build(BuildContext context, WidgetRef ref) { - final playState = ref.watch(playStateProvider); - final player = ref.read(playerProvider.notifier); - final playPauseController = useAnimationController( - duration: const Duration(milliseconds: 200), - initialValue: 1, + final playerStatus = + ref.watch(playerStatusProvider.select((v) => v.playStatus)); + + return PlatformIconButton( + icon: _getIcon(playerStatus, context), + onPressed: () => _actionButtonPressed(playerStatus, ref), ); - if (playState.playing) { - playPauseController.forward(); - } else { - playPauseController.reverse(); + } + + 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); + } + } + + 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(); } - return switch (playState.processingState) { - ProcessingState.loading || ProcessingState.buffering => const Padding( - padding: EdgeInsets.all(AppElementSizes.paddingRegular), - child: CircularProgressIndicator(), - ), - ProcessingState.completed => IconButton( - onPressed: () async { - await player.seekInBook(const Duration(seconds: 0)); - await player.play(); - }, - icon: const Icon( - Icons.replay, - ), - ), - ProcessingState.ready => IconButton( - onPressed: () async { - await player.togglePlayPause(); - }, - iconSize: iconSize, - icon: AnimatedIcon( - icon: AnimatedIcons.play_pause, - progress: playPauseController, - ), - ), - ProcessingState.idle => const SizedBox.shrink(), - }; } } diff --git a/lib/features/player/view/widgets/player_progress_bar.dart b/lib/features/player/view/widgets/player_progress_bar.dart index d5f7818..1b35bc2 100644 --- a/lib/features/player/view/widgets/player_progress_bar.dart +++ b/lib/features/player/view/widgets/player_progress_bar.dart @@ -1,10 +1,9 @@ import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; +import 'package:vaani/constants/sizes.dart'; +import 'package:vaani/features/player/providers/session_provider.dart'; class AudiobookChapterProgressBar extends HookConsumerWidget { const AudiobookChapterProgressBar({ @@ -13,8 +12,8 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); - final currentChapter = ref.watch(currentPlayingChapterProvider); + final player = ref.watch(playerProvider); + final currentChapter = ref.watch(currentChapterProvider); final position = useStream( player.positionStreamInBook, initialData: const Duration(seconds: 0), @@ -38,7 +37,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { progress: currentChapterProgress ?? position.data ?? const Duration(seconds: 0), total: currentChapter == null - ? player.book?.duration ?? const Duration(seconds: 0) + ? player.session?.duration ?? const Duration(seconds: 0) : currentChapter.end - currentChapter.start, // ! TODO add onSeek onSeek: (duration) { @@ -64,19 +63,19 @@ class AudiobookProgressBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); + final player = ref.watch(playerProvider); final position = useStream( player.slowPositionStreamInBook, initialData: const Duration(seconds: 0), ); - return ProgressBar( - progress: position.data ?? const Duration(seconds: 0), - total: player.book?.duration ?? const Duration(seconds: 0), - thumbRadius: 8, - bufferedBarColor: Theme.of(context).colorScheme.secondary, - timeLabelType: TimeLabelType.remainingTime, - timeLabelLocation: TimeLabelLocation.below, + return SizedBox( + height: AppElementSizes.barHeightLarge, + child: LinearProgressIndicator( + value: (position.data ?? const Duration(seconds: 0)).inSeconds / + (player.session?.duration ?? const Duration(seconds: 0)).inSeconds, + borderRadius: BorderRadiusGeometry.all(Radius.circular(10)), + ), ); } } diff --git a/lib/features/skip_start_end/player_skip_chapter_start_end.dart b/lib/features/player/view/widgets/player_skip_chapter_start_end.dart similarity index 81% rename from lib/features/skip_start_end/player_skip_chapter_start_end.dart rename to lib/features/player/view/widgets/player_skip_chapter_start_end.dart index 9712bdc..943f27c 100644 --- a/lib/features/skip_start_end/player_skip_chapter_start_end.dart +++ b/lib/features/player/view/widgets/player_skip_chapter_start_end.dart @@ -1,8 +1,11 @@ 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/audiobook_player.dart'; +import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/settings/view/notification_settings_page.dart'; class SkipChapterStartEndButton extends HookConsumerWidget { @@ -11,15 +14,16 @@ class SkipChapterStartEndButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Tooltip( - message: "跳过片头片尾", + message: S.of(context).chapterSkip, child: IconButton( - icon: const Icon(Icons.fast_forward_rounded), + // icon: const Icon(Icons.fast_forward_rounded), + icon: const Icon(FontAwesome.arrow_right_to_bracket_solid), onPressed: () async { // show toast pendingPlayerModals++; await showModalBottomSheet( context: context, - barrierLabel: '跳过片头片尾', + barrierLabel: S.of(context).chapterSkip, constraints: BoxConstraints( // 40% of the screen height maxHeight: MediaQuery.of(context).size.height * 0.4, @@ -43,15 +47,16 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); - final bookId = player.book?.libraryItemId ?? '_'; + final session = ref.watch(sessionProvider); + final bookId = session?.libraryItemId ?? '_'; final bookSettings = ref.watch(bookSettingsProvider(bookId)); return Scaffold( body: Column( children: [ ListTile( title: Text( - '跳过片头 ${bookSettings.playerSettings.skipChapterStart.inSeconds}s'), + '${S.of(context).chapterSkipOpen}${bookSettings.playerSettings.skipChapterStart.inSeconds}s', + ), ), Expanded( child: TimeIntervalSlider( @@ -75,7 +80,8 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget { ), ListTile( title: Text( - '跳过片尾 ${bookSettings.playerSettings.skipChapterEnd.inSeconds}s'), + '${S.of(context).chapterSkipEnd}${bookSettings.playerSettings.skipChapterEnd.inSeconds}s', + ), ), Expanded( child: TimeIntervalSlider( diff --git a/lib/features/shake_detection/providers/shake_detector.dart b/lib/features/shake_detection/providers/shake_detector.dart index 8892a92..9ff1548 100644 --- a/lib/features/shake_detection/providers/shake_detector.dart +++ b/lib/features/shake_detection/providers/shake_detector.dart @@ -2,8 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart' - show audiobookPlayerProvider, simpleAudiobookPlayerProvider; +import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' show sleepTimerProvider; import 'package:vaani/settings/app_settings_provider.dart' @@ -32,7 +31,7 @@ class ShakeDetector extends _$ShakeDetector { } // if no book is loaded, shake detection should not be enabled - final player = ref.watch(simpleAudiobookPlayerProvider); + final player = ref.watch(playerProvider); player.playerStateStream.listen((event) { if (event.processingState == ProcessingState.idle && wasPlayerLoaded) { _logger.config('Player is now not loaded, invalidating'); @@ -46,7 +45,7 @@ class ShakeDetector extends _$ShakeDetector { } }); - if (player.book == null) { + if (player.session == null) { _logger.config('No book is loaded, disabling shake detection'); wasPlayerLoaded = false; return null; @@ -87,8 +86,8 @@ class ShakeDetector extends _$ShakeDetector { ShakeAction shakeAction, { required Ref ref, }) { - final player = ref.read(simpleAudiobookPlayerProvider); - if (player.book == null && shakeAction.isPlaybackManagementEnabled) { + final player = ref.read(playerProvider); + if (player.session == null && shakeAction.isPlaybackManagementEnabled) { _logger.warning('No book is loaded'); return false; } @@ -104,19 +103,19 @@ class ShakeDetector extends _$ShakeDetector { return true; case ShakeAction.fastForward: _logger.fine('Fast forwarding'); - if (!player.playing) { + if (!player.player.playerState.playing) { _logger.warning('Player is not playing'); return false; } - player.seek(player.position + const Duration(seconds: 30)); + player.seek(player.player.position + const Duration(seconds: 30)); return true; case ShakeAction.rewind: _logger.fine('Rewinding'); - if (!player.playing) { + if (!player.player.playerState.playing) { _logger.warning('Player is not playing'); return false; } - player.seek(player.position - const Duration(seconds: 30)); + player.seek(player.player.position - const Duration(seconds: 30)); return true; case ShakeAction.playPause: _logger.fine('Toggling play/pause'); diff --git a/lib/features/shake_detection/providers/shake_detector.g.dart b/lib/features/shake_detection/providers/shake_detector.g.dart index ed81aaf..7cde527 100644 --- a/lib/features/shake_detection/providers/shake_detector.g.dart +++ b/lib/features/shake_detection/providers/shake_detector.g.dart @@ -6,7 +6,7 @@ part of 'shake_detector.dart'; // RiverpodGenerator // ************************************************************************** -String _$shakeDetectorHash() => r'2a380bab1d4021d05d2ae40fec964a5f33d3730c'; +String _$shakeDetectorHash() => r'd5f34001dbf6ffb2a114c877f05809c195a58e63'; /// See also [ShakeDetector]. @ProviderFor(ShakeDetector) diff --git a/lib/features/skip_start_end/skip_start_end.dart b/lib/features/skip_start_end/skip_start_end.dart index fcdb9ef..465961a 100644 --- a/lib/features/skip_start_end/skip_start_end.dart +++ b/lib/features/skip_start_end/skip_start_end.dart @@ -1,112 +1,48 @@ import 'dart:async'; -import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart'; +import 'package:vaani/features/player/core/audiobook_player_session.dart'; import 'package:vaani/shared/extensions/chapter.dart'; import 'package:vaani/shared/utils/throttler.dart'; class SkipStartEnd { final Duration start; final Duration end; - final AudiobookPlayer player; - // 当前章节的id - int? chapterId; - // int _index; + final AbsAudioHandler player; + final List _subscriptions = []; - final throttler = Throttler(delay: Duration(seconds: 3)); - // final StreamController _playbackController = - // StreamController.broadcast(); + final throttlerStart = Throttler(delay: Duration(seconds: 3)); + final throttlerEnd = Throttler(delay: Duration(seconds: 3)); SkipStartEnd({ required this.start, required this.end, required this.player, - this.chapterId, }) { - // if (start > Duration()) { - // _subscriptions.add( - // player.currentIndexStream.listen((index) { - // if (_index != index && player.position.inMilliseconds < 500) { - // Future.microtask(() { - // player.seek(start); - // }); - // _index = index!; - // } - // }), - // ); - // } - // if (end > Duration()) { - // _subscriptions.add( - // player.positionStream.distinct().listen((position) { - // if (player.duration != null && - // player.duration!.inMilliseconds - player.position.inMilliseconds < - // end.inMilliseconds) { - // throttler.call(() { - // print('跳过片尾'); - // Future.microtask(() async { - // await player.stop(); - // player.seekToNext(); - // }); - // }); - // } - // }), - // ); - // } - if (start > Duration.zero || end > Duration.zero) { + if (start > Duration.zero) { _subscriptions.add( - player.positionStream.listen((position) { - final chapter = player.currentChapter; - if (chapter == null) { - return; - } - if (chapter.id == chapterId) { - if (end > Duration.zero && - chapter.duration - (player.positionInBook - chapter.start) < - end) { - throttler.call(() { - Future.microtask(() => skipEnd(chapter)); - }); - } - } - if (chapter.id != chapterId) { - if (start > Duration.zero && - player.positionInBook - chapter.start < Duration(seconds: 1)) { - throttler.call(() { - Future.microtask(() => skipStart(chapter)); - }); - } - - chapterId = chapter.id; + player.chapterStream.listen((chapter) { + if (chapter != null && + player.positionInChapter < Duration(seconds: 1)) { + Future.microtask( + () => throttlerStart + .call(() => player.seekInBook(chapter.start + start)), + ); } }), ); } - } - - void skipStart(BookChapter chapter) { - print('跳过片头'); - final globalPosition = player.positionInBook; - if (globalPosition - chapter.start < Duration(seconds: 1)) { - player.seekInBook(chapter.start + start); - } - } - - void skipEnd(chapter) { - print('跳过片尾'); - final book = player.book; - if (book == null) { - return; - } - if (start > Duration.zero) { - final currentIndex = book.chapters.indexOf(chapter); - if (currentIndex < book.chapters.length - 1) { - final nextChapter = book.chapters[currentIndex + 1]; - // 跳过片头+片尾 - print('跳过片头+片尾'); - player.skipToChapter(nextChapter.id, position: start); - } - } else { - player.seekToPrevious(); + if (end > Duration.zero) { + _subscriptions.add( + player.positionStreamInChapter.listen((positionChapter) { + if (end > + (player.currentChapter?.duration ?? Duration.zero) - + positionChapter) { + Future.microtask( + () => throttlerEnd.call(() => player.skipToNext()), + ); + } + }), + ); } } @@ -115,7 +51,8 @@ class SkipStartEnd { for (var sub in _subscriptions) { sub.cancel(); } - throttler.dispose(); + throttlerStart.dispose(); + throttlerEnd.dispose(); // _playbackController.close(); } } diff --git a/lib/features/skip_start_end/skip_start_end_provider.dart b/lib/features/skip_start_end/skip_start_end_provider.dart index 9eed663..da7f9dc 100644 --- a/lib/features/skip_start_end/skip_start_end_provider.dart +++ b/lib/features/skip_start_end/skip_start_end_provider.dart @@ -1,6 +1,6 @@ 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/audiobook_player.dart'; +import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/skip_start_end/skip_start_end.dart' as core; part 'skip_start_end_provider.g.dart'; @@ -9,23 +9,51 @@ part 'skip_start_end_provider.g.dart'; class SkipStartEnd extends _$SkipStartEnd { @override core.SkipStartEnd? build() { - final player = ref.watch(simpleAudiobookPlayerProvider); - final book = ref.watch(audiobookPlayerProvider.select((v) => v.book)); - final bookId = book?.libraryItemId ?? '_'; - if (bookId == '_') { + final session = ref.watch(sessionProvider); + final bookId = session?.libraryItemId; + if (session == null || bookId == null) { return null; } + + final player = ref.read(playerProvider); 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, - chapterId: player.currentChapter?.id, ); ref.onDispose(skipStartEnd.dispose); return skipStartEnd; } } + +// @riverpod +// class SkipStartEnd extends _$SkipStartEnd { +// @override +// core.SkipStartEnd? build() { +// final player = ref.watch(simpleAudiobookPlayerProvider); +// final book = ref.watch(audiobookPlayerProvider.select((v) => v.book)); +// final bookId = book?.libraryItemId ?? '_'; +// if (bookId == '_') { +// return null; +// } +// final bookSettings = ref.watch(bookSettingsProvider(bookId)); +// final start = bookSettings.playerSettings.skipChapterStart; +// final end = bookSettings.playerSettings.skipChapterEnd; + +// final skipStartEnd = core.SkipStartEnd( +// start: start, +// end: end, +// player: player, +// chapterId: player.currentChapter?.id, +// ); +// ref.onDispose(skipStartEnd.dispose); +// return skipStartEnd; +// } +// } diff --git a/lib/features/skip_start_end/skip_start_end_provider.g.dart b/lib/features/skip_start_end/skip_start_end_provider.g.dart index 59771ef..4f5d2de 100644 --- a/lib/features/skip_start_end/skip_start_end_provider.g.dart +++ b/lib/features/skip_start_end/skip_start_end_provider.g.dart @@ -6,7 +6,7 @@ part of 'skip_start_end_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$skipStartEndHash() => r'857b448eac9bb9ab85cea9217775712e660bc990'; +String _$skipStartEndHash() => r'6df119db598c6e8673dcea090ad97f5affab4016'; /// See also [SkipStartEnd]. @ProviderFor(SkipStartEnd) diff --git a/lib/framework.dart b/lib/framework.dart index d389aad..855aa45 100644 --- a/lib/framework.dart +++ b/lib/framework.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart'; -import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; import 'package:vaani/features/player/core/audiobook_player_session.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/session_provider.dart'; @@ -87,24 +86,26 @@ class _FrameworkState extends ConsumerState Widget build(BuildContext context) { // Eagerly initialize providers by watching them. // By using "watch", the provider will stay alive and not be disposed. - final audioService = ref.watch(audioHandlerInitProvider); try { + final audioService = ref.watch(audioHandlerInitProvider); + ref.watch(playbackReporterProvider); // ref.watch(simpleAudiobookPlayerProvider); - // ref.watch(sleepTimerProvider); + ref.watch(sleepTimerProvider); // ref.watch(playbackReporterProvider); - // ref.watch(simpleDownloadManagerProvider); - // ref.watch(shakeDetectorProvider); - // ref.watch(skipStartEndProvider); + ref.watch(simpleDownloadManagerProvider); + if (Utils.isAndroid()) ref.watch(shakeDetectorProvider); + ref.watch(skipStartEndProvider); + return audioService.maybeWhen( + data: (_) { + return widget.child; + }, + orElse: () => SizedBox.shrink(), + ); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); appLogger.severe(e.toString()); + return SizedBox.shrink(); } - return audioService.maybeWhen( - data: (_) { - return widget.child; - }, - orElse: () => SizedBox.shrink(), - ); } @override diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 2cc5bdc..331e549 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -134,6 +134,18 @@ class MessageLookup extends MessageLookupByLibrary { "No shelves to display", ), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), + "chapterNotFound": MessageLookupByLibrary.simpleMessage("Chapters"), + "chapterSelect": MessageLookupByLibrary.simpleMessage("Select Chapter"), + "chapterSkip": MessageLookupByLibrary.simpleMessage( + "Skip chapter opening and ending", + ), + "chapterSkipEnd": MessageLookupByLibrary.simpleMessage( + "Skip chapter opening for ", + ), + "chapterSkipOpen": MessageLookupByLibrary.simpleMessage( + "Skip chapter opening for ", + ), + "chapters": MessageLookupByLibrary.simpleMessage("Chapters"), "copyToClipboard": MessageLookupByLibrary.simpleMessage( "Copy to Clipboard", ), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 2cc62a5..5de2d92 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -108,6 +108,12 @@ class MessageLookup extends MessageLookupByLibrary { "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"), "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"), "cancel": MessageLookupByLibrary.simpleMessage("取消"), + "chapterNotFound": MessageLookupByLibrary.simpleMessage("未找到章节"), + "chapterSelect": MessageLookupByLibrary.simpleMessage("选择章节"), + "chapterSkip": MessageLookupByLibrary.simpleMessage("跳过章节片头片尾"), + "chapterSkipEnd": MessageLookupByLibrary.simpleMessage("跳过章节片尾 "), + "chapterSkipOpen": MessageLookupByLibrary.simpleMessage("跳过章节片头 "), + "chapters": MessageLookupByLibrary.simpleMessage("章节列表"), "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( "将应用程序设置复制到剪贴板", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index ce8355a..cbfe6ca 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -489,6 +489,61 @@ class S { return Intl.message('Downloads', name: 'bookDownloads', desc: '', args: []); } + /// `Select Chapter` + String get chapterSelect { + return Intl.message( + 'Select Chapter', + name: 'chapterSelect', + desc: '', + args: [], + ); + } + + /// `Chapters` + String get chapters { + return Intl.message('Chapters', name: 'chapters', desc: '', args: []); + } + + /// `Chapters` + String get chapterNotFound { + return Intl.message( + 'Chapters', + name: 'chapterNotFound', + desc: '', + args: [], + ); + } + + /// `Skip chapter opening and ending` + String get chapterSkip { + return Intl.message( + 'Skip chapter opening and ending', + name: 'chapterSkip', + desc: '', + args: [], + ); + } + + /// `Skip chapter opening for ` + String get chapterSkipOpen { + return Intl.message( + 'Skip chapter opening for ', + name: 'chapterSkipOpen', + desc: '', + args: [], + ); + } + + /// `Skip chapter opening for ` + String get chapterSkipEnd { + return Intl.message( + 'Skip chapter opening for ', + name: 'chapterSkipEnd', + desc: '', + args: [], + ); + } + /// `Library` String get library { return Intl.message('Library', name: 'library', desc: '', args: []); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index bb3c524..cb6ab76 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -90,6 +90,13 @@ "bookSeries": "Series", "bookDownloads": "Downloads", + "chapterSelect": "Select Chapter", + "chapters": "Chapters", + "chapterNotFound": "Chapters", + "chapterSkip": "Skip chapter opening and ending", + "chapterSkipOpen": "Skip chapter opening for ", + "chapterSkipEnd": "Skip chapter opening for ", + "library": "Library", "libraryTooltip": "Browse your library", "librarySwitchTooltip": "Switch Library", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 022fe0a..d4fd895 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -90,6 +90,13 @@ "bookSeries": "系列", "bookDownloads": "下载", + "chapterSelect": "选择章节", + "chapters": "章节列表", + "chapterNotFound": "未找到章节", + "chapterSkip": "跳过章节片头片尾", + "chapterSkipOpen": "跳过章节片头 ", + "chapterSkipEnd": "跳过章节片尾 ", + "library": "媒体库", "libraryTooltip": "浏览您的媒体库", "librarySwitchTooltip": "切换媒体库", diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index a4e075b..978016c 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -4,6 +4,7 @@ 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/player_form.dart'; +import 'package:vaani/features/player/providers/session_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'; @@ -53,9 +54,10 @@ class ScaffoldWithNavBar extends HookConsumerWidget { } Widget buildNavLeft(BuildContext context, WidgetRef ref) { - final isPlayerActive = ref.watch(isPlayerActiveProvider); + // final isPlayerActive = ref.watch(isPlayerActiveProvider); + final session = ref.watch(sessionProvider); return Padding( - padding: EdgeInsets.only(bottom: isPlayerActive ? playerMinHeight : 0), + padding: EdgeInsets.only(bottom: session != null ? playerMinHeight : 0), child: Row( children: [ SafeArea( diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index c8a4411..ec5a745 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -11,6 +11,7 @@ import 'package:vaani/api/image_provider.dart'; 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/player_status_provider.dart'; import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/router/models/library_item_extras.dart'; import 'package:vaani/router/router.dart'; @@ -213,11 +214,12 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final me = ref.watch(meProvider); // final player = ref.watch(audiobookPlayerProvider); - final session = ref.watch(sessionProvider.select((v) => v.session)); - final sessionLoading = ref.watch(sessionLoadingProvider(libraryItemId)); - final playerState = ref.watch(playStateProvider); + final session = ref.watch(sessionProvider); + final playerStatus = ref.watch(playerStatusProvider); + final isLoading = playerStatus.isLoading(libraryItemId); final isCurrentBookSetInPlayer = session?.libraryItemId == libraryItemId; - final isPlayingThisBook = playerState.playing && isCurrentBookSetInPlayer; + final isPlayingThisBook = + playerStatus.isPlaying() && isCurrentBookSetInPlayer; final userProgress = me.valueOrNull?.mediaProgress ?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId); @@ -288,12 +290,14 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { ), ), onPressed: () => session?.libraryItemId == libraryItemId - ? ref.read(sessionProvider).load(libraryItemId, null) - : ref.read(playerProvider).togglePlayPause(), + ? ref.read(playerProvider).togglePlayPause() + : ref + .read(sessionProvider.notifier) + .load(libraryItemId, null), icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + libraryItemId, child: DynamicItemPlayIcon( - isLoading: sessionLoading, + isLoading: isLoading, isBookCompleted: isBookCompleted, isPlayingThisBook: isPlayingThisBook, isCurrentBookSetInPlayer: isCurrentBookSetInPlayer, @@ -340,7 +344,7 @@ class BookCoverWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider).session; + final session = ref.watch(sessionProvider); if (session == null) { return const BookCoverSkeleton(); }