From eb1955e5e622921150a9f4e8dd73cd96c37d83c8 Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Wed, 19 Nov 2025 17:43:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/library_item_actions.dart | 122 +-- .../core/playback_reporter_session.dart | 277 ++++++ .../player/core/audiobook_player.dart | 28 +- .../player/core/audiobook_player_session.dart | 208 +++-- lib/features/player/core/init.dart | 116 +-- .../player/providers/audiobook_player.dart | 14 +- .../player/providers/audiobook_player.g.dart | 2 +- .../providers/currently_playing_provider.dart | 4 +- .../currently_playing_provider.g.dart | 2 +- .../player/providers/session_provider.dart | 216 +++++ .../player/providers/session_provider.g.dart | 253 +++++ lib/features/player/view/player_expanded.dart | 36 +- .../player/view/player_minimized.dart | 42 +- .../widgets/player_player_pause_button.dart | 11 +- lib/framework.dart | 156 ++++ lib/generated/intl/messages_en.dart | 872 +++++++++--------- lib/generated/intl/messages_zh.dart | 659 +++++++------ lib/main.dart | 87 +- lib/models/tray.dart | 1 - lib/router/scaffold_with_nav_bar.dart | 115 ++- lib/shared/widgets/shelves/book_shelf.dart | 53 +- pubspec.lock | 41 +- pubspec.yaml | 29 +- .../flutter/generated_plugin_registrant.cc | 6 +- windows/flutter/generated_plugins.cmake | 2 +- 25 files changed, 2102 insertions(+), 1250 deletions(-) create mode 100644 lib/features/playback_reporting/core/playback_reporter_session.dart create mode 100644 lib/features/player/providers/session_provider.dart create mode 100644 lib/features/player/providers/session_provider.g.dart create mode 100644 lib/framework.dart diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index ffde277..a97cb1a 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -12,17 +12,14 @@ import 'package:vaani/features/downloads/providers/download_manager.dart' downloadManagerProvider, isItemDownloadedProvider, isItemDownloadingProvider, - itemDownloadProgressProvider, - simpleDownloadManagerProvider; + itemDownloadProgressProvider; import 'package:vaani/features/item_viewer/view/library_item_page.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/player_form.dart'; +import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/utils.dart'; @@ -302,7 +299,7 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null; + final isBookPlaying = ref.watch(playStateProvider).playing; return IconButton( onPressed: () { @@ -435,9 +432,14 @@ class _LibraryItemPlayButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final book = item.media.asBookExpanded; - final player = ref.watch(audiobookPlayerProvider); - final isCurrentBookSetInPlayer = player.book == book; - final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; + 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 isCurrentBookSetInPlayer = + session?.libraryItemId == book.libraryItemId; + final isPlayingThisBook = playerState.playing && isCurrentBookSetInPlayer; final userMediaProgress = item.userMediaProgress; final isBookCompleted = userMediaProgress?.isFinished ?? false; @@ -464,14 +466,13 @@ class _LibraryItemPlayButton extends HookConsumerWidget { } return ElevatedButton.icon( - onPressed: () => libraryItemPlayButtonOnPressed( - ref: ref, - book: book, - userMediaProgress: userMediaProgress, - ), + onPressed: () => session?.libraryItemId == book.libraryItemId + ? ref.read(sessionProvider).load(book.libraryItemId, null) + : ref.read(playerProvider).togglePlayPause(), icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + book.libraryItemId, child: DynamicItemPlayIcon( + isLoading: sessionLoading, isCurrentBookSetInPlayer: isCurrentBookSetInPlayer, isPlayingThisBook: isPlayingThisBook, isBookCompleted: isBookCompleted, @@ -493,87 +494,32 @@ class DynamicItemPlayIcon extends StatelessWidget { required this.isCurrentBookSetInPlayer, required this.isPlayingThisBook, required this.isBookCompleted, + this.isLoading = false, }); final bool isCurrentBookSetInPlayer; final bool isPlayingThisBook; final bool isBookCompleted; + final bool isLoading; @override Widget build(BuildContext context) { - return Icon( - isCurrentBookSetInPlayer - ? isPlayingThisBook - ? Icons.pause_rounded - : Icons.play_arrow_rounded - : isBookCompleted - ? Icons.replay_rounded - : Icons.play_arrow_rounded, - ); + return isLoading + ? SizedBox( + // width: 20, + // height: 20, + child: CircularProgressIndicator( + strokeWidth: 4, + ), + ) + : Icon( + isCurrentBookSetInPlayer + ? isPlayingThisBook + ? Icons.pause_rounded + : Icons.play_arrow_rounded + : isBookCompleted + ? Icons.replay_rounded + : Icons.play_arrow_rounded, + ); } } - -/// Handles the play button pressed on the library item -Future libraryItemPlayButtonOnPressed({ - required WidgetRef ref, - required shelfsdk.BookExpanded book, - shelfsdk.MediaProgress? userMediaProgress, -}) async { - appLogger.info('Pressed play/resume button'); - final player = ref.watch(audiobookPlayerProvider); - // final bookSettings = ref.watch(bookSettingsProvider(book.libraryItemId)); - - final isCurrentBookSetInPlayer = player.book == book; - final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; - - Future? setSourceFuture; - // set the book to the player if not already set - if (!isCurrentBookSetInPlayer) { - appLogger.info('Setting the book ${book.libraryItemId}'); - appLogger.info('Initial position: ${userMediaProgress?.currentTime}'); - final downloadManager = ref.watch(simpleDownloadManagerProvider); - final libItem = - await ref.read(libraryItemProvider(book.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); - setSourceFuture = player.setSourceAudiobook( - book, - initialPosition: userMediaProgress?.currentTime, - downloadedUris: downloadedUris, - ); - } else { - appLogger.info('Book was already set'); - if (isPlayingThisBook) { - appLogger.info('Pausing the book'); - await player.pause(); - return; - } - } - // set the volume as this is the first time playing and dismissing causes the volume to go to 0 - var bookPlayerSettings = - ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings; - var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - - var configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; - - await Future.wait([ - setSourceFuture ?? Future.value(), - // set the volume - player.setVolume( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultVolume ?? - appPlayerSettings.preferredDefaultVolume - : appPlayerSettings.preferredDefaultVolume, - ), - // set the speed - player.setSpeed( - configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed - : appPlayerSettings.preferredDefaultSpeed, - ), - ]); - - // toggle play/pause - await player.play(); -} diff --git a/lib/features/playback_reporting/core/playback_reporter_session.dart b/lib/features/playback_reporting/core/playback_reporter_session.dart new file mode 100644 index 0000000..e002d28 --- /dev/null +++ b/lib/features/playback_reporting/core/playback_reporter_session.dart @@ -0,0 +1,277 @@ +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/shared/extensions/obfuscation.dart'; + +final _logger = Logger('PlaybackReporter'); + +/// this playback reporter will watch the player and report to the server +/// +/// it will by default report every 10 seconds +/// and also report when the player is paused/stopped/finished/playing +class PlaybackReporter { + /// The player to watch + final AudiobookPlayer player; + + /// the api to report to + final AudiobookshelfApi authenticatedApi; + + /// The stopwatch to keep track of the time since the last report + /// + /// this should only run when media is playing + final _stopwatch = Stopwatch(); + + /// subscriptions to listen and then cancel when disposing + final List _subscriptions = []; + + Duration _reportingInterval; + + /// the duration to wait before reporting + Duration get reportingInterval => _reportingInterval; + set reportingInterval(Duration value) { + _reportingInterval = value; + _cancelReportTimer(); + _setReportTimerIfNotAlready(); + _logger.info('set interval: $value'); + } + + /// the minimum duration to report + final Duration reportingDurationThreshold; + + /// the duration to wait before starting the reporting + /// this is to ignore the initial duration in case user is browsing + final Duration? minimumPositionForReporting; + + /// the duration to mark the book as complete when the time left is less than this + final Duration markCompleteWhenTimeLeft; + + /// timer to report every 10 seconds + /// tracking the time since the last report + Timer? _reportTimer; + + PlaybackReporter( + this.player, + this.authenticatedApi, { + required PlaybackSession session, + this.reportingDurationThreshold = const Duration(seconds: 1), + Duration reportingInterval = const Duration(seconds: 10), + this.minimumPositionForReporting, + this.markCompleteWhenTimeLeft = const Duration(seconds: 5), + }) : _reportingInterval = reportingInterval, + _session = session { + // initial conditions + 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(); + _cancelReportTimer(); + } + + // start or stop the stopwatch based on the playing state + if (state.playing) { + _stopwatch.start(); + _logger.fine( + 'player state observed, starting stopwatch at ${_stopwatch.elapsed}', + ); + } else if (!state.playing) { + _stopwatch.stop(); + _logger.fine( + 'player state observed, stopping stopwatch at ${_stopwatch.elapsed}', + ); + await tryReportPlayback(null); + } + }), + ); + + _logger.fine( + 'initialized with reportingInterval: $reportingInterval, reportingDurationThreshold: $reportingDurationThreshold', + ); + _logger.fine( + 'initialized with minimumPositionForReporting: $minimumPositionForReporting, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft', + ); + } + + Future tryReportPlayback(_) async { + _logger.fine( + 'callback called when elapsed ${_stopwatch.elapsed}', + ); + if (player.book != null && + player.positionInBook >= + player.book!.duration - markCompleteWhenTimeLeft) { + _logger.info( + 'marking complete as time left is less than $markCompleteWhenTimeLeft', + ); + await markComplete(); + return; + } + if (_stopwatch.elapsed > reportingDurationThreshold) { + _logger.fine( + 'reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold', + ); + await syncCurrentPosition(); + } + } + + /// dispose the timer + Future dispose() async { + for (var sub in _subscriptions) { + sub.cancel(); + } + await closeSession(); + _stopwatch.stop(); + _reportTimer?.cancel(); + + _logger.fine('disposed'); + } + + /// current sessionId + /// this is used to report the playback + PlaybackSession? _session; + String? get sessionId => _session?.id; + + Future markComplete() async { + if (player.book == null) { + throw NoAudiobookPlayingError(); + } + await authenticatedApi.me.createUpdateMediaProgress( + libraryItemId: player.book!.libraryItemId, + parameters: CreateUpdateProgressReqParams( + isFinished: true, + currentTime: player.positionInBook, + duration: player.book!.duration, + ), + responseErrorHandler: _responseErrorHandler, + ); + _logger.info('Marked complete for book: ${player.book!.libraryItemId}'); + } + + Future syncCurrentPosition() async { + final data = _getSyncData(); + if (data == null) { + await closeSession(); + } + + final currentPosition = player.positionInBook; + + await authenticatedApi.sessions.syncOpen( + sessionId: sessionId!, + parameters: _getSyncData()!, + responseErrorHandler: _responseErrorHandler, + ); + + _logger.fine( + 'Synced position: $currentPosition with timeListened: ${_stopwatch.elapsed} for session: $sessionId', + ); + + // reset the stopwatch + _stopwatch.reset(); + } + + Future closeSession() async { + if (sessionId == null) { + _logger.warning('No session to close'); + return; + } + + await authenticatedApi.sessions.closeOpen( + sessionId: sessionId!, + parameters: _getSyncData(), + responseErrorHandler: _responseErrorHandler, + ); + _session = null; + _logger.info('Closed session'); + } + + void _setReportTimerIfNotAlready() { + if (_reportTimer != null) return; + _reportTimer = Timer.periodic(_reportingInterval, tryReportPlayback); + _logger.fine('set timer with interval: $_reportingInterval'); + } + + void _cancelReportTimer() { + _reportTimer?.cancel(); + _reportTimer = null; + _logger.fine('cancelled timer'); + } + + void _responseErrorHandler(http.Response response, [error]) { + if (response.statusCode != 200) { + _logger.severe('Error with api: ${response.obfuscate()}, $error'); + throw PlaybackSyncError( + 'Error syncing position: ${response.body}, $error', + ); + } + } + + SyncSessionReqParams? _getSyncData() { + 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 && + player.positionInBook < minimumPositionForReporting!) { + // but if elapsed time is more than the minimumPositionForReporting, sync + if (_stopwatch.elapsed > minimumPositionForReporting!) { + _logger.info( + 'Syncing position despite being less than minimumPositionForReporting as elapsed time is more: ${_stopwatch.elapsed}', + ); + } else { + _logger.info( + 'Ignoring sync for position: ${player.positionInBook} < $minimumPositionForReporting', + ); + return null; + } + } + + return SyncSessionReqParams( + currentTime: player.positionInBook, + timeListened: _stopwatch.elapsed, + duration: player.book?.duration ?? Duration.zero, + ); + } +} + +class PlaybackSyncError implements Exception { + String message; + + PlaybackSyncError([this.message = 'Error syncing playback']); + + @override + String toString() { + return 'PlaybackSyncError: $message'; + } +} + +class NoAudiobookPlayingError implements Exception { + String message; + + NoAudiobookPlayingError([this.message = 'No audiobook is playing']); + + @override + String toString() { + return 'NoAudiobookPlayingError: $message'; + } +} diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 9aa349b..f179c72 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -5,7 +5,7 @@ library; import 'package:collection/collection.dart'; import 'package:just_audio/just_audio.dart'; -import 'package:just_audio_background/just_audio_background.dart'; +// import 'package:just_audio_background/just_audio_background.dart'; import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; @@ -124,19 +124,19 @@ class AudiobookPlayer extends AudioPlayer { // ); return AudioSource.uri( retrievedUri, - tag: MediaItem( - // Specify a unique ID for each media item: - id: book.libraryItemId + track.index.toString(), - // Metadata to display in the notification: - title: appSettings.notificationSettings.primaryTitle - .formatNotificationTitle(book), - album: appSettings.notificationSettings.secondaryTitle - .formatNotificationTitle(book), - artUri: artworkUri ?? - Uri.parse( - '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', - ), - ), + // tag: MediaItem( + // // Specify a unique ID for each media item: + // id: book.libraryItemId + track.index.toString(), + // // Metadata to display in the notification: + // title: appSettings.notificationSettings.primaryTitle + // .formatNotificationTitle(book), + // album: appSettings.notificationSettings.secondaryTitle + // .formatNotificationTitle(book), + // artUri: artworkUri ?? + // Uri.parse( + // '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', + // ), + // ), ); }).toList(); await setAudioSources( diff --git a/lib/features/player/core/audiobook_player_session.dart b/lib/features/player/core/audiobook_player_session.dart index eba1622..0485657 100644 --- a/lib/features/player/core/audiobook_player_session.dart +++ b/lib/features/player/core/audiobook_player_session.dart @@ -1,33 +1,30 @@ // my_audio_handler.dart +import 'dart:io'; + import 'package:audio_service/audio_service.dart'; 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:shelfsdk/audiobookshelf_api.dart'; +import 'package:vaani/features/player/providers/session_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 final offset = Duration(milliseconds: 10); -class HookAudioHandler extends BaseAudioHandler { +class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final AudioPlayer _player = AudioPlayer(); - final List _playlist = []; + // final List _playlist = []; final Ref ref; - BookExpanded? _book; + PlaybackSessionExpanded? _session; - /// the authentication token to access the [AudioTrack.contentUrl] - final String token; - - /// the base url for the audio files - final Uri baseUrl; - - HookAudioHandler(this.ref, {required this.token, required this.baseUrl}) { + AbsAudioHandler(this.ref) { _setupAudioPlayer(); } void _setupAudioPlayer() { - _player.setAudioSources(_playlist); - // // 监听播放位置变化,更新全局位置 // _player.positionStream.listen((position) { // // _updateGlobalPosition(position); @@ -42,49 +39,58 @@ class HookAudioHandler extends BaseAudioHandler { // 转发播放状态 _player.playbackEventStream.map(_transformEvent).pipe(playbackState); + _player.playerStateStream.distinct().listen((event) { + ref.read(playStateProvider.notifier).setState(event); + }); } // 加载有声书 Future setSourceAudiobook( - BookExpanded audiobook, { - Duration? initialPosition, + PlaybackSessionExpanded playbackSession, { + required Uri baseUrl, + required String token, List? downloadedUris, }) async { - _book = audiobook; - - // 清空现有播放列表 - _playlist.clear(); + _session = playbackSession; // 添加所有音轨 - for (final track in audiobook.tracks) { - final audioSource = ProgressiveAudioSource( - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), - tag: MediaItem( - id: '${audiobook.libraryItemId}${track.index}', - title: track.title, - duration: track.duration, + List audioSources = []; + for (final track in playbackSession.audioTracks) { + audioSources.add( + AudioSource.uri( + _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), ), ); - _playlist.add(audioSource); } - // 初始化队列显示 - final mediaItems = audiobook.tracks - .map( - (track) => MediaItem( - id: '${audiobook.libraryItemId}${track.index}', - title: track.title, - duration: track.duration, - ), - ) - .toList(); - - queue.add(mediaItems); + playMediaItem( + MediaItem( + id: playbackSession.libraryItemId, + album: playbackSession.mediaMetadata.title, + title: playbackSession.displayTitle, + displaySubtitle: playbackSession.mediaType == MediaType.book + ? (playbackSession.mediaMetadata as BookMetadata).subtitle + : null, + duration: playbackSession.duration, + artUri: Uri.parse( + '$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token', + ), + ), + ); + final track = playbackSession.findTrackAtTime(playbackSession.currentTime); + final index = playbackSession.audioTracks.indexOf(track); + await _player.setAudioSources( + audioSources, + initialIndex: index, + initialPosition: playbackSession.currentTime - track.startOffset, + ); + _player.seek(playbackSession.currentTime - track.startOffset, index: index); + await play(); // 恢复上次播放位置(如果有) - if (initialPosition != null) { - await seekToPosition(initialPosition); - } + // if (initialPosition != null) { + // await seekInBook(initialPosition); + // } } // // 音轨切换处理 @@ -97,19 +103,19 @@ class HookAudioHandler extends BaseAudioHandler { // 核心功能:跳转到指定章节 Future skipToChapter(int chapterId) async { - if (_book == null) return; + if (_session == null) return; - final chapter = _book!.chapters.firstWhere( + final chapter = _session!.chapters.firstWhere( (ch) => ch.id == chapterId, orElse: () => throw Exception('Chapter not found'), ); - await seekToPosition(chapter.start + offset); + await seekInBook(chapter.start + offset); } Duration get positionInBook { - if (_book != null && _player.currentIndex != null) { - return _book!.tracks[_player.currentIndex!].startOffset + + if (_session != null && _player.currentIndex != null) { + return _session!.audioTracks[_player.currentIndex!].startOffset + _player.position; } return Duration.zero; @@ -117,31 +123,61 @@ class HookAudioHandler extends BaseAudioHandler { // 当前音轨 AudioTrack? get currentTrack { - if (_book == null) { + if (_session == null) { return null; } - return _book!.findTrackAtTime(positionInBook); + return _session!.findTrackAtTime(positionInBook); } // 当前章节 BookChapter? get currentChapter { - if (_book == null) { + if (_session == null) { return null; } - return _book!.findChapterAtTime(positionInBook); + return _session!.findChapterAtTime(positionInBook); + } + + Duration? get chapterDuration => currentChapter?.duration; + Stream get positionStream => _player.positionStream; + Stream get positionStreamInChapter { + 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; + }); + } + + Future togglePlayPause() { + // check if book is set + if (_session == null) { + return Future.value(); + } + + return switch (_player.playerState) { + PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), + }; } // 播放控制方法 @override - Future play() => _player.play(); + Future play() async { + await _player.play(); + } @override - Future pause() => _player.pause(); + Future pause() async { + await _player.pause(); + } // 重写上一曲/下一曲为章节导航 @override Future skipToNext() async { - if (_book == null) { + if (_session == null) { // 回退到默认行为 return _player.seekToNext(); } @@ -150,17 +186,17 @@ class HookAudioHandler extends BaseAudioHandler { // 回退到默认行为 return _player.seekToNext(); } - final currentIndex = _book!.chapters.indexOf(chapter); - if (currentIndex < _book!.chapters.length - 1) { + final chapterIndex = _session!.chapters.indexOf(chapter); + if (chapterIndex < _session!.chapters.length - 1) { // 跳到下一章 - final nextChapter = _book!.chapters[currentIndex + 1]; + final nextChapter = _session!.chapters[chapterIndex + 1]; await skipToChapter(nextChapter.id); } } @override Future skipToPrevious() async { - if (_book == null) { + if (_session == null) { return _player.seekToPrevious(); } @@ -168,14 +204,14 @@ class HookAudioHandler extends BaseAudioHandler { if (chapter == null) { return _player.seekToPrevious(); } - final currentIndex = _book!.chapters.indexOf(chapter); + final currentIndex = _session!.chapters.indexOf(chapter); if (currentIndex > 0) { // 跳到上一章 - final prevChapter = _book!.chapters[currentIndex - 1]; + final prevChapter = _session!.chapters[currentIndex - 1]; await skipToChapter(prevChapter.id); } else { // 已经是第一章,回到开头 - await seekToPosition(Duration.zero); + await seekInBook(Duration.zero); } } @@ -188,15 +224,24 @@ class HookAudioHandler extends BaseAudioHandler { if (track != null) { startOffset = track.startOffset; } - await seekToPosition(startOffset + position); + await seekInBook(startOffset + position); + } + + Future setVolume(double volume) async { + await _player.setVolume(volume); + } + + @override + Future setSpeed(double speed) async { + await _player.setSpeed(speed); } // 核心功能:跳转到全局时间位置 - Future seekToPosition(Duration globalPosition) async { - if (_book == null) return; + Future seekInBook(Duration globalPosition) async { + if (_session == null) return; // 找到目标音轨和在音轨内的位置 - final track = _book!.findTrackAtTime(globalPosition); - final index = _book!.tracks.indexOf(track); + final track = _session!.findTrackAtTime(globalPosition); + final index = _session!.audioTracks.indexOf(track); Duration positionInTrack = globalPosition - track.startOffset; if (positionInTrack <= Duration.zero) { positionInTrack = offset; @@ -205,13 +250,26 @@ class HookAudioHandler extends BaseAudioHandler { await _player.seek(positionInTrack, index: index); } + AudioPlayer get player => _player; PlaybackState _transformEvent(PlaybackEvent event) { return PlaybackState( controls: [ - MediaControl.skipToPrevious, + if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious, + MediaControl.rewind, if (_player.playing) MediaControl.pause else MediaControl.play, - MediaControl.skipToNext, + MediaControl.stop, + MediaControl.fastForward, + if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext, ], + systemActions: { + if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious, + MediaAction.rewind, + MediaAction.fastForward, + MediaAction.stop, + MediaAction.setSpeed, + if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext, + }, + androidCompactActionIndices: const [1, 2, 3], processingState: const { ProcessingState.idle: AudioProcessingState.idle, ProcessingState.loading: AudioProcessingState.loading, @@ -225,6 +283,7 @@ class HookAudioHandler extends BaseAudioHandler { bufferedPosition: _player.bufferedPosition, speed: _player.speed, queueIndex: event.currentIndex, + captioningEnabled: false, ); } } @@ -246,7 +305,7 @@ Uri _getUri( Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); } -extension BookExpandedExtension on BookExpanded { +extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded { BookChapter findChapterAtTime(Duration position) { return chapters.firstWhere( (element) { @@ -257,16 +316,23 @@ extension BookExpandedExtension on BookExpanded { } AudioTrack findTrackAtTime(Duration position) { - return tracks.firstWhere( + return audioTracks.firstWhere( (element) { return element.startOffset <= position && element.startOffset + element.duration >= position + offset; }, - orElse: () => tracks.first, + orElse: () => audioTracks.first, ); } + int findTrackIndexAtTime(Duration position) { + return audioTracks.indexWhere((element) { + return element.startOffset <= position && + element.startOffset + element.duration >= position + offset; + }); + } + Duration getTrackStartOffset(int index) { - return tracks[index].startOffset; + return audioTracks[index].startOffset; } } diff --git a/lib/features/player/core/init.dart b/lib/features/player/core/init.dart index 7b6c0f7..8891556 100644 --- a/lib/features/player/core/init.dart +++ b/lib/features/player/core/init.dart @@ -1,62 +1,62 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:audio_session/audio_session.dart'; -import 'package:just_audio_background/just_audio_background.dart' - show JustAudioBackground, NotificationConfig; -import 'package:just_audio_media_kit/just_audio_media_kit.dart' - show JustAudioMediaKit; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/settings/models/app_settings.dart'; +// import 'package:audio_service/audio_service.dart'; +// import 'package:audio_session/audio_session.dart'; +// import 'package:just_audio_background/just_audio_background.dart' +// show JustAudioBackground, NotificationConfig; +// import 'package:just_audio_media_kit/just_audio_media_kit.dart' +// show JustAudioMediaKit; +// import 'package:vaani/settings/app_settings_provider.dart'; +// import 'package:vaani/settings/models/app_settings.dart'; -Future configurePlayer() async { - // for playing audio on windows, linux - JustAudioMediaKit.ensureInitialized(windows: false); +// Future configurePlayer() async { +// // for playing audio on windows, linux +// JustAudioMediaKit.ensureInitialized(windows: false); - // for configuring how this app will interact with other audio apps - final session = await AudioSession.instance; - await session.configure(const AudioSessionConfiguration.speech()); +// // for configuring how this app will interact with other audio apps +// final session = await AudioSession.instance; +// await session.configure(const AudioSessionConfiguration.speech()); - final appSettings = loadOrCreateAppSettings(); +// final appSettings = loadOrCreateAppSettings(); - // for playing audio in the background - await JustAudioBackground.init( - androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio', - androidNotificationChannelName: 'Audio playback', - androidNotificationOngoing: false, - androidStopForegroundOnPause: false, - androidNotificationChannelDescription: 'Audio playback in the background', - androidNotificationIcon: 'drawable/ic_stat_logo', - rewindInterval: appSettings.notificationSettings.rewindInterval, - fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, - androidShowNotificationBadge: false, - notificationConfigBuilder: (state) { - final controls = [ - if (appSettings.notificationSettings.mediaControls - .contains(NotificationMediaControl.skipToPreviousChapter) && - state.hasPrevious) - MediaControl.skipToPrevious, - if (appSettings.notificationSettings.mediaControls - .contains(NotificationMediaControl.rewind)) - MediaControl.rewind, - if (state.playing) MediaControl.pause else MediaControl.play, - if (appSettings.notificationSettings.mediaControls - .contains(NotificationMediaControl.fastForward)) - MediaControl.fastForward, - if (appSettings.notificationSettings.mediaControls - .contains(NotificationMediaControl.skipToNextChapter) && - state.hasNext) - MediaControl.skipToNext, - if (appSettings.notificationSettings.mediaControls - .contains(NotificationMediaControl.stop)) - MediaControl.stop, - ]; - return NotificationConfig( - controls: controls, - systemActions: const { - MediaAction.seek, - MediaAction.seekForward, - MediaAction.seekBackward, - }, - ); - }, - ); -} +// // for playing audio in the background +// await JustAudioBackground.init( +// androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio', +// androidNotificationChannelName: 'Audio playback', +// androidNotificationOngoing: false, +// androidStopForegroundOnPause: false, +// androidNotificationChannelDescription: 'Audio playback in the background', +// androidNotificationIcon: 'drawable/ic_stat_logo', +// rewindInterval: appSettings.notificationSettings.rewindInterval, +// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, +// androidShowNotificationBadge: false, +// notificationConfigBuilder: (state) { +// final controls = [ +// if (appSettings.notificationSettings.mediaControls +// .contains(NotificationMediaControl.skipToPreviousChapter) && +// state.hasPrevious) +// MediaControl.skipToPrevious, +// if (appSettings.notificationSettings.mediaControls +// .contains(NotificationMediaControl.rewind)) +// MediaControl.rewind, +// if (state.playing) MediaControl.pause else MediaControl.play, +// if (appSettings.notificationSettings.mediaControls +// .contains(NotificationMediaControl.fastForward)) +// MediaControl.fastForward, +// if (appSettings.notificationSettings.mediaControls +// .contains(NotificationMediaControl.skipToNextChapter) && +// state.hasNext) +// MediaControl.skipToNext, +// if (appSettings.notificationSettings.mediaControls +// .contains(NotificationMediaControl.stop)) +// MediaControl.stop, +// ]; +// return NotificationConfig( +// controls: controls, +// systemActions: const { +// MediaAction.seek, +// MediaAction.seekForward, +// MediaAction.seekBackward, +// }, +// ); +// }, +// ); +// } diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index 98eb38b..6335535 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -1,6 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/features/player/core/audiobook_player.dart' as core; @@ -38,9 +39,9 @@ class AudiobookPlayer extends _$AudiobookPlayer { ref.onDispose(player.dispose); // bind notify listeners to the player - player.playerStateStream.listen((_) { - ref.notifyListeners(); - }); + // player.playerStateStream.listen((_) { + // ref.notifyListeners(); + // }); _logger.finer('created player'); @@ -51,6 +52,13 @@ class AudiobookPlayer extends _$AudiobookPlayer { await state.setSpeed(speed); ref.notifyListeners(); } + + Future setSourceAudiobook({ + required shelfsdk.BookExpanded book, + shelfsdk.MediaProgress? userMediaProgress, + }) async { + ref.notifyListeners(); + } } @riverpod diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index a3af070..a4f5451 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -43,7 +43,7 @@ final simpleAudiobookPlayerProvider = ); typedef _$SimpleAudiobookPlayer = Notifier; -String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0'; +String _$audiobookPlayerHash() => r'04448247e79c5d60b9fd6f98eeeb865f1e8d0ff8'; /// See also [AudiobookPlayer]. @ProviderFor(AudiobookPlayer) diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index a9beabd..dc44548 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -12,8 +12,8 @@ final _logger = Logger('CurrentlyPlayingProvider'); @riverpod BookExpanded? currentlyPlayingBook(Ref ref) { try { - final player = ref.watch(audiobookPlayerProvider); - return player.book; + final book = ref.watch(simpleAudiobookPlayerProvider.select((v) => v.book)); + return book; } catch (e) { _logger.warning('Error getting currently playing book: $e'); return null; diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart index ace77bb..430089a 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart'; // ************************************************************************** String _$currentlyPlayingBookHash() => - r'e4258694c8f0d1e89651b330fae0f672ca13a484'; + r'f2c47028340d253be9440dc29f835328ff30c0e6'; /// See also [currentlyPlayingBook]. @ProviderFor(currentlyPlayingBook) diff --git a/lib/features/player/providers/session_provider.dart b/lib/features/player/providers/session_provider.dart new file mode 100644 index 0000000..fb218bd --- /dev/null +++ b/lib/features/player/providers/session_provider.dart @@ -0,0 +1,216 @@ +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'; +import 'package:shelfsdk/audiobookshelf_api.dart' as core; +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/audiobook_player_session.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; + } + + Future load(String id, String? episodeId) async { + ref.read(sessionLoadingProvider(id).notifier).setLoading(); + final api = ref.read(authenticatedApiProvider); + final playBack = await api.items.play( + libraryItemId: id, + parameters: core.PlayItemReqParams( + deviceInfo: core.DeviceInfoReqParams( + clientVersion: appVersion, + manufacturer: deviceManufacturer, + model: deviceModel, + sdkVersion: deviceSdkVersion, + clientName: appName, + deviceName: deviceName, + ), + forceDirectPlay: false, + forceTranscode: false, + supportedMimeTypes: [ + "audio/flac", + "audio/mpeg", + "audio/mp4", + "audio/ogg", + "audio/aac", + "audio/webm", + ], + ), + responseErrorHandler: _responseErrorHandler, + ) as core.PlaybackSessionExpanded; + + final downloadManager = ref.read(simpleDownloadManagerProvider); + final libItem = + await ref.read(libraryItemProvider(playBack.libraryItemId).future); + final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); + + var bookPlayerSettings = + ref.read(bookSettingsProvider(playBack.libraryItemId)).playerSettings; + var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; + + var configurePlayerForEveryBook = + appPlayerSettings.configurePlayerForEveryBook; + + await Future.wait([ + _audioService.setSourceAudiobook( + playBack, + baseUrl: api.baseUrl, + token: api.token!, + downloadedUris: downloadedUris, + ), + // set the volume + _audioService.setVolume( + configurePlayerForEveryBook + ? bookPlayerSettings.preferredDefaultVolume ?? + appPlayerSettings.preferredDefaultVolume + : appPlayerSettings.preferredDefaultVolume, + ), + // set the speed + _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'); + throw PlaybackSyncError( + 'Error syncing position: ${response.body}, $error', + ); + } + } +} + +@Riverpod(keepAlive: true) +class Player extends _$Player { + @override + AbsAudioHandler build() { + final audioService = ref.watch(sessionProvider).audioService; + // audioService.positionStream.listen((position){ + + // }); + return audioService; + } + + Future togglePlayPause() => state.togglePlayPause(); + Future play() => state.play(); + Future pause() => state.pause(); + Future seekInBook(Duration globalPosition) => + state.seekInBook(globalPosition); +} + +@Riverpod(keepAlive: true) +SessionPlayer session(Ref ref) { + return SessionPlayer(ref); +} + +@Riverpod(keepAlive: true) +class SessionLoading extends _$SessionLoading { + @override + bool build(String itemId) { + return false; + } + + setLoading() { + state = true; + } + + setLoaded() { + state = false; + } +} + +@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; + + PlaybackSyncError([this.message = 'Error syncing playback']); + + @override + String toString() { + return 'PlaybackSyncError: $message'; + } +} diff --git a/lib/features/player/providers/session_provider.g.dart b/lib/features/player/providers/session_provider.g.dart new file mode 100644 index 0000000..4ecebde --- /dev/null +++ b/lib/features/player/providers/session_provider.g.dart @@ -0,0 +1,253 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +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'; + +/// See also [audioHandlerInit]. +@ProviderFor(audioHandlerInit) +final audioHandlerInitProvider = FutureProvider.internal( + audioHandlerInit, + name: r'audioHandlerInitProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$audioHandlerInitHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AudioHandlerInitRef = FutureProviderRef; +String _$playerHash() => r'41cc626fd4a3317ce7e1ffa3c5e03206a9819231'; + +/// See also [Player]. +@ProviderFor(Player) +final playerProvider = NotifierProvider.internal( + Player.new, + name: r'playerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Player = Notifier; +String _$sessionLoadingHash() => r'4688469dd8ac9f38063917ede032cfe1506a63a8'; + +/// 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', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PlayState = 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/view/player_expanded.dart b/lib/features/player/view/player_expanded.dart index b60213f..2648b30 100644 --- a/lib/features/player/view/player_expanded.dart +++ b/lib/features/player/view/player_expanded.dart @@ -3,10 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/api/image_provider.dart'; -import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/constants/sizes.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.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'; @@ -28,32 +26,20 @@ class PlayerExpanded extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(sessionProvider).session; + if (session == null) { + return SizedBox.shrink(); + } + /// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer] /// however, some properties need to start later than 0% and end before 100% - final currentBook = ref.watch(currentlyPlayingBookProvider); - if (currentBook == null) { - return const SizedBox.shrink(); - } - final currentChapter = ref.watch(currentPlayingChapterProvider); - final currentBookMetadata = ref.watch(currentBookMetadataProvider); + final currentChapter = ref.watch(currentChapterProvider); + // final currentBookMetadata = ref.watch(currentBookMetadataProvider); // max height of the player is the height of the screen final playerMaxHeight = MediaQuery.of(context).size.height; final availWidth = MediaQuery.of(context).size.width; // the image width when the player is expanded final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9); - final itemBeingPlayed = - ref.watch(libraryItemProvider(currentBook.libraryItemId)); - final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null - ? ref.watch( - coverImageProvider(itemBeingPlayed.valueOrNull!.id), - ) - : null; - final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null - ? Image.memory( - imageOfItemBeingPlayed!.valueOrNull!, - fit: BoxFit.cover, - ) - : const BookCoverSkeleton(); return Scaffold( appBar: AppBar( leading: IconButton( @@ -104,7 +90,7 @@ class PlayerExpanded extends HookConsumerWidget { borderRadius: BorderRadius.circular( AppElementSizes.borderRadiusRegular, ), - child: imgWidget, + child: BookCoverWidget(), ), ), ), @@ -133,8 +119,8 @@ class PlayerExpanded extends HookConsumerWidget { padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular), child: Text( [ - currentBookMetadata?.title ?? '', - currentBookMetadata?.authorName ?? '', + session.displayTitle, + session.displayAuthor, ].join(' - '), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context) diff --git a/lib/features/player/view/player_minimized.dart b/lib/features/player/view/player_minimized.dart index 409176e..638ea90 100644 --- a/lib/features/player/view/player_minimized.dart +++ b/lib/features/player/view/player_minimized.dart @@ -1,13 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/api/image_provider.dart'; -import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/constants/sizes.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/providers/session_provider.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; @@ -20,25 +16,11 @@ class PlayerMinimized extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentBook = ref.watch(currentlyPlayingBookProvider); - if (currentBook == null) { - return const SizedBox.shrink(); + final session = ref.watch(sessionProvider).session; + if (session == null) { + return SizedBox.shrink(); } - final itemBeingPlayed = - ref.watch(libraryItemProvider(currentBook.libraryItemId)); - final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null - ? ref.watch( - coverImageProvider(itemBeingPlayed.valueOrNull!.id), - ) - : null; - final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null - ? Image.memory( - imageOfItemBeingPlayed!.valueOrNull!, - fit: BoxFit.cover, - ) - : const BookCoverSkeleton(); - final bookMetaExpanded = ref.watch(currentBookMetadataProvider); - final currentChapter = ref.watch(currentPlayingChapterProvider); + final currentChapter = ref.watch(currentChapterProvider); return PlayerMinimizedFramework( children: [ @@ -51,7 +33,7 @@ class PlayerMinimized extends HookConsumerWidget { context.pushNamed( Routes.libraryItem.name, pathParameters: { - Routes.libraryItem.pathParamName!: currentBook.libraryItemId, + Routes.libraryItem.pathParamName!: session.libraryItemId, }, ); }, @@ -59,7 +41,7 @@ class PlayerMinimized extends HookConsumerWidget { constraints: BoxConstraints( maxWidth: playerMinimizedHeight, ), - child: imgWidget, + child: BookCoverWidget(), ), ), ), @@ -76,14 +58,14 @@ class PlayerMinimized extends HookConsumerWidget { children: [ // AutoScrollText( Text( - '${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}', + '${session.displayTitle} - ${currentChapter?.title ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, // velocity: // const Velocity(pixelsPerSecond: Offset(16, 0)), style: Theme.of(context).textTheme.bodyLarge, ), Text( - bookMetaExpanded?.authorName ?? '', + session.displayAuthor, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -127,9 +109,9 @@ class PlayerMinimizedFramework extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); + final player = ref.watch(playerProvider); final progress = - useStream(player.positionStream, initialData: Duration.zero); + useStream(player.positionStreamInChapter, initialData: Duration.zero); return GestureDetector( onTap: () => context.pushNamed(Routes.player.name), child: Container( @@ -147,7 +129,7 @@ class PlayerMinimizedFramework extends HookConsumerWidget { // value: (progress.data ?? Duration.zero).inSeconds / // player.book!.duration.inSeconds, value: (progress.data ?? Duration.zero).inSeconds / - (player.duration?.inSeconds ?? 1), + (player.chapterDuration?.inSeconds ?? 1), color: Theme.of(context).colorScheme.onPrimaryContainer, backgroundColor: Theme.of(context).colorScheme.primaryContainer, ), 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 ab26b65..dd928bb 100644 --- a/lib/features/player/view/widgets/player_player_pause_button.dart +++ b/lib/features/player/view/widgets/player_player_pause_button.dart @@ -1,10 +1,9 @@ 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:just_audio/just_audio.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 AudiobookPlayerPlayPauseButton extends HookConsumerWidget { const AudiobookPlayerPlayPauseButton({ @@ -15,18 +14,18 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { final double iconSize; @override Widget build(BuildContext context, WidgetRef ref) { - final player = ref.watch(audiobookPlayerProvider); - final playing = ref.watch(isPlayerPlayingProvider); + final playState = ref.watch(playStateProvider); + final player = ref.read(playerProvider.notifier); final playPauseController = useAnimationController( duration: const Duration(milliseconds: 200), initialValue: 1, ); - if (playing) { + if (playState.playing) { playPauseController.forward(); } else { playPauseController.reverse(); } - return switch (player.processingState) { + return switch (playState.processingState) { ProcessingState.loading || ProcessingState.buffering => const Padding( padding: EdgeInsets.all(AppElementSizes.paddingRegular), child: CircularProgressIndicator(), diff --git a/lib/framework.dart b/lib/framework.dart new file mode 100644 index 0000000..d389aad --- /dev/null +++ b/lib/framework.dart @@ -0,0 +1,156 @@ +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'; +import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; +import 'package:vaani/features/skip_start_end/skip_start_end_provider.dart'; +import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; +import 'package:vaani/globals.dart'; +import 'package:vaani/shared/utils/utils.dart'; +import 'package:window_manager/window_manager.dart'; + +class Framework extends ConsumerStatefulWidget { + final Widget child; + final AbsAudioHandler? audioHandler; + const Framework({required this.child, this.audioHandler, super.key}); + + @override + ConsumerState createState() => _FrameworkState(); +} + +class _FrameworkState extends ConsumerState + with TrayListener, WindowListener { + @override + void initState() { + if (Utils.isDesktop()) { + windowManager.addListener(this); + _init(); + } + super.initState(); + } + + @override + void dispose() { + trayManager.removeListener(this); + super.dispose(); + } + + void _init() async { + await trayManager.setIcon( + Utils.isWindows() ? 'assets/icon/logo.ico' : 'assets/icon/logo.png', + ); + await trayManager.setToolTip(appName); + Menu menu = Menu( + items: [ + MenuItem( + key: 'show_window', + // label: 'Show Window', + label: '显示主窗口', + onClick: (menuItem) => windowManager.show(), + ), + MenuItem.separator(), + MenuItem( + key: 'play_pause', + label: '播放/暂停', + onClick: (menuItem) => + ref.read(audiobookPlayerProvider).togglePlayPause(), + ), + MenuItem( + key: 'previous', + label: '上一个', + onClick: (menuItem) => + ref.read(audiobookPlayerProvider).seekToPrevious(), + ), + MenuItem( + key: 'next', + label: '下一个', + onClick: (menuItem) => ref.read(audiobookPlayerProvider).seekToNext(), + ), + MenuItem.separator(), + MenuItem( + key: 'exit_app', + // label: 'Exit App', + label: '退出', + onClick: (menuItem) => windowManager.destroy(), + ), + ], + ); + await trayManager.setContextMenu(menu); + trayManager.addListener(this); + } + + @override + 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 { + // ref.watch(simpleAudiobookPlayerProvider); + // ref.watch(sleepTimerProvider); + // ref.watch(playbackReporterProvider); + // ref.watch(simpleDownloadManagerProvider); + // ref.watch(shakeDetectorProvider); + // ref.watch(skipStartEndProvider); + } catch (e) { + debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); + appLogger.severe(e.toString()); + } + return audioService.maybeWhen( + data: (_) { + return widget.child; + }, + orElse: () => SizedBox.shrink(), + ); + } + + @override + void onTrayIconMouseDown() { + // do something, for example pop up the menu + // print('onTrayIconMouseDown'); + windowManager.show(); + } + + @override + void onTrayIconMouseUp() { + // do something, for example pop up the menu + // print('onTrayIconMouseUp'); + } + + @override + void onTrayIconRightMouseDown() { + // do something + // print('onTrayIconRightMouseDown'); + trayManager.popUpContextMenu(bringAppToFront: true); + } + + @override + void onTrayIconRightMouseUp() { + // do something + // print('onTrayIconRightMouseUp'); + } + + // @override + // void onTrayMenuItemClick(MenuItem menuItem) { + // print(menuItem.key); + // if (menuItem.key == 'show_window') { + // // do something + // } else if (menuItem.key == 'exit_app') { + // // do something + + // } else if (menuItem.key == 'play_pause'){ + + // } + // } + + @override + void onWindowClose() async { + final isPreventClose = await windowManager.isPreventClose(); + if (isPreventClose) { + windowManager.hide(); + } + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 2f73cc4..2cc5bdc 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -38,471 +38,449 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "account": MessageLookupByLibrary.simpleMessage("Account"), - "accountAddNewServer": MessageLookupByLibrary.simpleMessage( - "Add New Server", - ), - "accountAddUser": MessageLookupByLibrary.simpleMessage("Add User"), - "accountAddUserDialog": m0, - "accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage( - "User added successfully! Switch?", - ), - "accountAddUserTooltip": MessageLookupByLibrary.simpleMessage( - "Add new server", - ), - "accountAnonymous": MessageLookupByLibrary.simpleMessage("Anonymous"), - "accountDeleteServer": MessageLookupByLibrary.simpleMessage( - "Delete Server", - ), - "accountInvalidURL": - MessageLookupByLibrary.simpleMessage("Invalid URL"), - "accountManage": - MessageLookupByLibrary.simpleMessage("Manage Accounts"), - "accountRegisteredServers": MessageLookupByLibrary.simpleMessage( - "Registered Servers", - ), - "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( - "Remove Server and Users", - ), - "accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage( - "This will remove the server ", - ), - "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( - " and all its users\' login info from this app.", - ), - "accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage( - "Remove User Login", - ), - "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( - "This will remove login details of the user ", - ), - "accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage( - " from this app.", - ), - "accountServerURI": MessageLookupByLibrary.simpleMessage("Server URI"), - "accountSwitch": MessageLookupByLibrary.simpleMessage("Switch Account"), - "accountUsersCount": m1, - "appSettings": MessageLookupByLibrary.simpleMessage("App Settings"), - "appearance": MessageLookupByLibrary.simpleMessage("Appearance"), - "autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage( - "Auto Sleep Timer Settings", - ), - "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage( - "Auto Turn On Sleep Timer", - ), - "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage( - "Auto Turn On Timer", - ), - "autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage( - "Always Auto Turn On Timer", - ), - "autoTurnOnTimerAlwaysDescription": - MessageLookupByLibrary.simpleMessage( - "Always turn on the sleep timer, no matter what", - ), - "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( - "Automatically turn on the sleep timer based on the time of day", - ), - "autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage("From"), - "autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage( - "Turn on the sleep timer at the specified time", - ), - "autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("Until"), - "autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage( - "Turn off the sleep timer at the specified time", - ), - "automaticallyDescription": MessageLookupByLibrary.simpleMessage( - "Automatically turn on the sleep timer based on the time of day", - ), - "backup": MessageLookupByLibrary.simpleMessage("Backup"), - "backupAndRestore": MessageLookupByLibrary.simpleMessage( - "Backup and Restore", - ), - "bookAbout": MessageLookupByLibrary.simpleMessage("About the Book"), - "bookAboutDefault": MessageLookupByLibrary.simpleMessage( - "Sorry, no description found", - ), - "bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"), - "bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"), - "bookGenres": MessageLookupByLibrary.simpleMessage("Genres"), - "bookMetadataAbridged": - MessageLookupByLibrary.simpleMessage("Abridged"), - "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), - "bookMetadataPublished": - MessageLookupByLibrary.simpleMessage("Published"), - "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( - "Unabridged", - ), - "bookSeries": MessageLookupByLibrary.simpleMessage("Series"), - "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("Try again"), - "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage( - "No shelves to display", - ), - "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), - "copyToClipboard": MessageLookupByLibrary.simpleMessage( - "Copy to Clipboard", - ), - "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( - "Copy the app settings to the clipboard", - ), - "copyToClipboardToast": MessageLookupByLibrary.simpleMessage( - "Settings copied to clipboard", - ), - "delete": MessageLookupByLibrary.simpleMessage("Delete"), - "deleteDialog": m2, - "deleted": m3, - "explore": MessageLookupByLibrary.simpleMessage("explore"), - "exploreHint": MessageLookupByLibrary.simpleMessage( - "Seek and you shall discover...", - ), - "exploreTooltip": MessageLookupByLibrary.simpleMessage( - "Search and Explore", - ), - "general": MessageLookupByLibrary.simpleMessage("General"), - "help": MessageLookupByLibrary.simpleMessage("Help"), - "home": MessageLookupByLibrary.simpleMessage("Home"), - "homeBookContinueListening": MessageLookupByLibrary.simpleMessage( - "Continue Listening", - ), - "homeBookContinueListeningDescription": - MessageLookupByLibrary.simpleMessage( + "account": MessageLookupByLibrary.simpleMessage("Account"), + "accountAddNewServer": MessageLookupByLibrary.simpleMessage( + "Add New Server", + ), + "accountAddUser": MessageLookupByLibrary.simpleMessage("Add User"), + "accountAddUserDialog": m0, + "accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage( + "User added successfully! Switch?", + ), + "accountAddUserTooltip": MessageLookupByLibrary.simpleMessage( + "Add new server", + ), + "accountAnonymous": MessageLookupByLibrary.simpleMessage("Anonymous"), + "accountDeleteServer": MessageLookupByLibrary.simpleMessage( + "Delete Server", + ), + "accountInvalidURL": MessageLookupByLibrary.simpleMessage("Invalid URL"), + "accountManage": MessageLookupByLibrary.simpleMessage("Manage Accounts"), + "accountRegisteredServers": MessageLookupByLibrary.simpleMessage( + "Registered Servers", + ), + "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( + "Remove Server and Users", + ), + "accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage( + "This will remove the server ", + ), + "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( + " and all its users\' login info from this app.", + ), + "accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage( + "Remove User Login", + ), + "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( + "This will remove login details of the user ", + ), + "accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage( + " from this app.", + ), + "accountServerURI": MessageLookupByLibrary.simpleMessage("Server URI"), + "accountSwitch": MessageLookupByLibrary.simpleMessage("Switch Account"), + "accountUsersCount": m1, + "appSettings": MessageLookupByLibrary.simpleMessage("App Settings"), + "appearance": MessageLookupByLibrary.simpleMessage("Appearance"), + "autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage( + "Auto Sleep Timer Settings", + ), + "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage( + "Auto Turn On Sleep Timer", + ), + "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage( + "Auto Turn On Timer", + ), + "autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage( + "Always Auto Turn On Timer", + ), + "autoTurnOnTimerAlwaysDescription": MessageLookupByLibrary.simpleMessage( + "Always turn on the sleep timer, no matter what", + ), + "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( + "Automatically turn on the sleep timer based on the time of day", + ), + "autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage("From"), + "autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage( + "Turn on the sleep timer at the specified time", + ), + "autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("Until"), + "autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage( + "Turn off the sleep timer at the specified time", + ), + "automaticallyDescription": MessageLookupByLibrary.simpleMessage( + "Automatically turn on the sleep timer based on the time of day", + ), + "backup": MessageLookupByLibrary.simpleMessage("Backup"), + "backupAndRestore": MessageLookupByLibrary.simpleMessage( + "Backup and Restore", + ), + "bookAbout": MessageLookupByLibrary.simpleMessage("About the Book"), + "bookAboutDefault": MessageLookupByLibrary.simpleMessage( + "Sorry, no description found", + ), + "bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"), + "bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"), + "bookGenres": MessageLookupByLibrary.simpleMessage("Genres"), + "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"), + "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), + "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"), + "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( + "Unabridged", + ), + "bookSeries": MessageLookupByLibrary.simpleMessage("Series"), + "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("Try again"), + "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage( + "No shelves to display", + ), + "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), + "copyToClipboard": MessageLookupByLibrary.simpleMessage( + "Copy to Clipboard", + ), + "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( + "Copy the app settings to the clipboard", + ), + "copyToClipboardToast": MessageLookupByLibrary.simpleMessage( + "Settings copied to clipboard", + ), + "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "deleteDialog": m2, + "deleted": m3, + "explore": MessageLookupByLibrary.simpleMessage("explore"), + "exploreHint": MessageLookupByLibrary.simpleMessage( + "Seek and you shall discover...", + ), + "exploreTooltip": MessageLookupByLibrary.simpleMessage( + "Search and Explore", + ), + "general": MessageLookupByLibrary.simpleMessage("General"), + "help": MessageLookupByLibrary.simpleMessage("Help"), + "home": MessageLookupByLibrary.simpleMessage("Home"), + "homeBookContinueListening": MessageLookupByLibrary.simpleMessage( + "Continue Listening", + ), + "homeBookContinueListeningDescription": + MessageLookupByLibrary.simpleMessage( "Show play button for books in currently listening shelf", ), - "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage( - "Continue Series", - ), - "homeBookContinueSeriesDescription": - MessageLookupByLibrary.simpleMessage( - "Show play button for books in continue series shelf", - ), - "homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"), - "homeBookListenAgain": - MessageLookupByLibrary.simpleMessage("Listen Again"), - "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( - "Show play button for all books in listen again shelf", - ), - "homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage( - "Newest Authors", - ), - "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage( - "Recently Added", - ), - "homeBookRecommended": - MessageLookupByLibrary.simpleMessage("Recommended"), - "homeContinueListening": MessageLookupByLibrary.simpleMessage( - "Continue Listening", - ), - "homeListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), - "homePageSettings": MessageLookupByLibrary.simpleMessage( - "Home Page Settings", - ), - "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( - "Customize the home page", - ), - "homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage( - "Other shelves", - ), - "homePageSettingsOtherShelvesDescription": - MessageLookupByLibrary.simpleMessage( + "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage( + "Continue Series", + ), + "homeBookContinueSeriesDescription": MessageLookupByLibrary.simpleMessage( + "Show play button for books in continue series shelf", + ), + "homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"), + "homeBookListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), + "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( + "Show play button for all books in listen again shelf", + ), + "homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage( + "Newest Authors", + ), + "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage( + "Recently Added", + ), + "homeBookRecommended": MessageLookupByLibrary.simpleMessage("Recommended"), + "homeContinueListening": MessageLookupByLibrary.simpleMessage( + "Continue Listening", + ), + "homeListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), + "homePageSettings": MessageLookupByLibrary.simpleMessage( + "Home Page Settings", + ), + "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Customize the home page", + ), + "homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage( + "Other shelves", + ), + "homePageSettingsOtherShelvesDescription": + MessageLookupByLibrary.simpleMessage( "Show play button for all books in all remaining shelves", ), - "homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage( - "Quick Play", - ), - "homeStartListening": MessageLookupByLibrary.simpleMessage( - "Start Listening", - ), - "language": MessageLookupByLibrary.simpleMessage("Language"), - "languageDescription": MessageLookupByLibrary.simpleMessage( - "Language switch", - ), - "library": MessageLookupByLibrary.simpleMessage("Library"), - "libraryChange": MessageLookupByLibrary.simpleMessage("Change Library"), - "libraryEmpty": MessageLookupByLibrary.simpleMessage( - "No libraries available.", - ), - "libraryLoadError": m4, - "librarySelect": MessageLookupByLibrary.simpleMessage("Select Library"), - "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage( - "Switch Library", - ), - "libraryTooltip": MessageLookupByLibrary.simpleMessage( - "Browse your library", - ), - "loading": MessageLookupByLibrary.simpleMessage("Loading..."), - "loginLocal": MessageLookupByLibrary.simpleMessage("Local"), - "loginLogin": MessageLookupByLibrary.simpleMessage("Login"), - "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), - "loginPassword": MessageLookupByLibrary.simpleMessage("Password"), - "loginServerClick": MessageLookupByLibrary.simpleMessage("Click here"), - "loginServerConnected": MessageLookupByLibrary.simpleMessage( - "Server connected, please login", - ), - "loginServerNo": MessageLookupByLibrary.simpleMessage( - "Do not have a server? ", - ), - "loginServerNoConnected": MessageLookupByLibrary.simpleMessage( - "Please enter the URL of your AudiobookShelf Server", - ), - "loginServerNot": m5, - "loginServerTo": MessageLookupByLibrary.simpleMessage( - " to know how to setup a server.", - ), - "loginTitle": m6, - "loginToken": MessageLookupByLibrary.simpleMessage("Token"), - "loginUsername": MessageLookupByLibrary.simpleMessage("Username"), - "logs": MessageLookupByLibrary.simpleMessage("Logs"), - "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage( - "Backward Interval", - ), - "nmpSettingsForward": MessageLookupByLibrary.simpleMessage( - "Forward Interval", - ), - "nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage( - "Media Controls", - ), - "nmpSettingsMediaControlsDescription": - MessageLookupByLibrary.simpleMessage( - "Select the media controls to display", - ), - "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( - "Select a field below to insert it", - ), - "nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage( - "Show Chapter Progress", - ), - "nmpSettingsShowChapterProgressDescription": - MessageLookupByLibrary.simpleMessage( + "homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage( + "Quick Play", + ), + "homeStartListening": MessageLookupByLibrary.simpleMessage( + "Start Listening", + ), + "language": MessageLookupByLibrary.simpleMessage("Language"), + "languageDescription": MessageLookupByLibrary.simpleMessage( + "Language switch", + ), + "library": MessageLookupByLibrary.simpleMessage("Library"), + "libraryChange": MessageLookupByLibrary.simpleMessage("Change Library"), + "libraryEmpty": MessageLookupByLibrary.simpleMessage( + "No libraries available.", + ), + "libraryLoadError": m4, + "librarySelect": MessageLookupByLibrary.simpleMessage("Select Library"), + "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage( + "Switch Library", + ), + "libraryTooltip": MessageLookupByLibrary.simpleMessage( + "Browse your library", + ), + "loading": MessageLookupByLibrary.simpleMessage("Loading..."), + "loginLocal": MessageLookupByLibrary.simpleMessage("Local"), + "loginLogin": MessageLookupByLibrary.simpleMessage("Login"), + "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), + "loginPassword": MessageLookupByLibrary.simpleMessage("Password"), + "loginServerClick": MessageLookupByLibrary.simpleMessage("Click here"), + "loginServerConnected": MessageLookupByLibrary.simpleMessage( + "Server connected, please login", + ), + "loginServerNo": MessageLookupByLibrary.simpleMessage( + "Do not have a server? ", + ), + "loginServerNoConnected": MessageLookupByLibrary.simpleMessage( + "Please enter the URL of your AudiobookShelf Server", + ), + "loginServerNot": m5, + "loginServerTo": MessageLookupByLibrary.simpleMessage( + " to know how to setup a server.", + ), + "loginTitle": m6, + "loginToken": MessageLookupByLibrary.simpleMessage("Token"), + "loginUsername": MessageLookupByLibrary.simpleMessage("Username"), + "logs": MessageLookupByLibrary.simpleMessage("Logs"), + "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage( + "Backward Interval", + ), + "nmpSettingsForward": MessageLookupByLibrary.simpleMessage( + "Forward Interval", + ), + "nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage( + "Media Controls", + ), + "nmpSettingsMediaControlsDescription": MessageLookupByLibrary.simpleMessage( + "Select the media controls to display", + ), + "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( + "Select a field below to insert it", + ), + "nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage( + "Show Chapter Progress", + ), + "nmpSettingsShowChapterProgressDescription": + MessageLookupByLibrary.simpleMessage( "Instead of the overall progress of the book", ), - "nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage( - "Secondary Title", - ), - "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( - "The subtitle of the notification\n", - ), - "nmpSettingsTitle": - MessageLookupByLibrary.simpleMessage("Primary Title"), - "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( - "The title of the notification\n", - ), - "no": MessageLookupByLibrary.simpleMessage("No"), - "notImplemented": - MessageLookupByLibrary.simpleMessage("Not implemented"), - "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( - "Notification Media Player", - ), - "notificationMediaPlayerDescription": - MessageLookupByLibrary.simpleMessage( - "Customize the media player in notifications", - ), - "ok": MessageLookupByLibrary.simpleMessage("OK"), - "pause": MessageLookupByLibrary.simpleMessage("Pause"), - "play": MessageLookupByLibrary.simpleMessage("Play"), - "playerSettings": - MessageLookupByLibrary.simpleMessage("Player Settings"), - "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( - "Mark Complete When Time Left", - ), - "playerSettingsCompleteTimeDescriptionHead": - MessageLookupByLibrary.simpleMessage( - "Mark complete when less than "), - "playerSettingsCompleteTimeDescriptionTail": - MessageLookupByLibrary.simpleMessage(" left in the book"), - "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( - "Customize the player settings", - ), - "playerSettingsDisplay": MessageLookupByLibrary.simpleMessage( - "Display Settings", - ), - "playerSettingsDisplayChapterProgress": - MessageLookupByLibrary.simpleMessage("Show Chapter Progress"), - "playerSettingsDisplayChapterProgressDescription": - MessageLookupByLibrary.simpleMessage( + "nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage( + "Secondary Title", + ), + "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( + "The subtitle of the notification\n", + ), + "nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("Primary Title"), + "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( + "The title of the notification\n", + ), + "no": MessageLookupByLibrary.simpleMessage("No"), + "notImplemented": MessageLookupByLibrary.simpleMessage("Not implemented"), + "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( + "Notification Media Player", + ), + "notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( + "Customize the media player in notifications", + ), + "ok": MessageLookupByLibrary.simpleMessage("OK"), + "pause": MessageLookupByLibrary.simpleMessage("Pause"), + "play": MessageLookupByLibrary.simpleMessage("Play"), + "playerSettings": MessageLookupByLibrary.simpleMessage("Player Settings"), + "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( + "Mark Complete When Time Left", + ), + "playerSettingsCompleteTimeDescriptionHead": + MessageLookupByLibrary.simpleMessage("Mark complete when less than "), + "playerSettingsCompleteTimeDescriptionTail": + MessageLookupByLibrary.simpleMessage(" left in the book"), + "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Customize the player settings", + ), + "playerSettingsDisplay": MessageLookupByLibrary.simpleMessage( + "Display Settings", + ), + "playerSettingsDisplayChapterProgress": + MessageLookupByLibrary.simpleMessage("Show Chapter Progress"), + "playerSettingsDisplayChapterProgressDescription": + MessageLookupByLibrary.simpleMessage( "Show the progress of the current chapter in the player", ), - "playerSettingsDisplayTotalProgress": - MessageLookupByLibrary.simpleMessage( - "Show Total Progress", - ), - "playerSettingsDisplayTotalProgressDescription": - MessageLookupByLibrary.simpleMessage( + "playerSettingsDisplayTotalProgress": MessageLookupByLibrary.simpleMessage( + "Show Total Progress", + ), + "playerSettingsDisplayTotalProgressDescription": + MessageLookupByLibrary.simpleMessage( "Show the total progress of the book in the player", ), - "playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage( - "Playback Report Interval", - ), - "playerSettingsPlaybackIntervalDescriptionHead": - MessageLookupByLibrary.simpleMessage("Report progress every "), - "playerSettingsPlaybackIntervalDescriptionTail": - MessageLookupByLibrary.simpleMessage(" to the server"), - "playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage( - "Playback Reporting", - ), - "playerSettingsPlaybackReportingIgnore": - MessageLookupByLibrary.simpleMessage( + "playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage( + "Playback Report Interval", + ), + "playerSettingsPlaybackIntervalDescriptionHead": + MessageLookupByLibrary.simpleMessage("Report progress every "), + "playerSettingsPlaybackIntervalDescriptionTail": + MessageLookupByLibrary.simpleMessage(" to the server"), + "playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage( + "Playback Reporting", + ), + "playerSettingsPlaybackReportingIgnore": + MessageLookupByLibrary.simpleMessage( "Ignore Playback Position Less Than", ), - "playerSettingsPlaybackReportingMinimum": - MessageLookupByLibrary.simpleMessage("Minimum Position to Report"), - "playerSettingsPlaybackReportingMinimumDescriptionHead": - MessageLookupByLibrary.simpleMessage( + "playerSettingsPlaybackReportingMinimum": + MessageLookupByLibrary.simpleMessage("Minimum Position to Report"), + "playerSettingsPlaybackReportingMinimumDescriptionHead": + MessageLookupByLibrary.simpleMessage( "Do not report playback for the first ", ), - "playerSettingsPlaybackReportingMinimumDescriptionTail": - MessageLookupByLibrary.simpleMessage("of the book"), - "playerSettingsRememberForEveryBook": - MessageLookupByLibrary.simpleMessage( - "Remember Player Settings for Every Book", - ), - "playerSettingsRememberForEveryBookDescription": - MessageLookupByLibrary.simpleMessage( + "playerSettingsPlaybackReportingMinimumDescriptionTail": + MessageLookupByLibrary.simpleMessage("of the book"), + "playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage( + "Remember Player Settings for Every Book", + ), + "playerSettingsRememberForEveryBookDescription": + MessageLookupByLibrary.simpleMessage( "Settings like speed, loudness, etc. will be remembered for every book", ), - "playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("Speed"), - "playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage( - "Default Speed", - ), - "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( - "Speed Options", - ), - "playerSettingsSpeedOptionsSelect": - MessageLookupByLibrary.simpleMessage( - "Select Speed Options", - ), - "playerSettingsSpeedOptionsSelectAdd": - MessageLookupByLibrary.simpleMessage( - "Add Speed Option", - ), - "playerSettingsSpeedOptionsSelectAddHelper": - MessageLookupByLibrary.simpleMessage( - "Enter a new speed option to add"), - "playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage( - "Select Speed", - ), - "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( - "Enter the speed you want to set when playing for the first time", - ), - "playlistsMine": MessageLookupByLibrary.simpleMessage("My Playlists"), - "readLess": MessageLookupByLibrary.simpleMessage("Read Less"), - "readMore": MessageLookupByLibrary.simpleMessage("Read More"), - "refresh": MessageLookupByLibrary.simpleMessage("Refresh"), - "reset": MessageLookupByLibrary.simpleMessage("Reset"), - "resetAppSettings": MessageLookupByLibrary.simpleMessage( - "Reset App Settings", - ), - "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( - "Reset the app settings to the default values", - ), - "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( - "Are you sure you want to reset the app settings?", - ), - "restore": MessageLookupByLibrary.simpleMessage("Restore"), - "restoreBackup": MessageLookupByLibrary.simpleMessage("Restore Backup"), - "restoreBackupHint": MessageLookupByLibrary.simpleMessage( - "Paste the backup here", - ), - "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage( - "Invalid backup", - ), - "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage( - "Settings restored", - ), - "restoreBackupValidator": MessageLookupByLibrary.simpleMessage( - "Please paste the backup here", - ), - "restoreDescription": MessageLookupByLibrary.simpleMessage( - "Restore the app settings from the backup", - ), - "resume": MessageLookupByLibrary.simpleMessage("Resume"), - "retry": MessageLookupByLibrary.simpleMessage("Retry"), - "settings": MessageLookupByLibrary.simpleMessage("Settings"), - "shakeAction": MessageLookupByLibrary.simpleMessage("Shake Action"), - "shakeActionDescription": MessageLookupByLibrary.simpleMessage( - "The action to perform when a shake is detected", - ), - "shakeActivationThreshold": MessageLookupByLibrary.simpleMessage( - "Shake Activation Threshold", - ), - "shakeActivationThresholdDescription": - MessageLookupByLibrary.simpleMessage( - "The higher the threshold, the harder you need to shake", - ), - "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), - "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( - "Customize the shake detector settings", - ), - "shakeDetectorEnable": MessageLookupByLibrary.simpleMessage( - "Enable Shake Detection", - ), - "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( - "Enable shake detection to do various actions", - ), - "shakeDetectorSettings": MessageLookupByLibrary.simpleMessage( - "Shake Detector Settings", - ), - "shakeFeedback": MessageLookupByLibrary.simpleMessage("Shake Feedback"), - "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( - "The feedback to give when a shake is detected", - ), - "shakeSelectAction": MessageLookupByLibrary.simpleMessage( - "Select Shake Action", - ), - "shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage( - "Select Shake Activation Threshold", - ), - "shakeSelectActivationThresholdHelper": - MessageLookupByLibrary.simpleMessage( + "playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("Speed"), + "playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage( + "Default Speed", + ), + "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( + "Speed Options", + ), + "playerSettingsSpeedOptionsSelect": MessageLookupByLibrary.simpleMessage( + "Select Speed Options", + ), + "playerSettingsSpeedOptionsSelectAdd": MessageLookupByLibrary.simpleMessage( + "Add Speed Option", + ), + "playerSettingsSpeedOptionsSelectAddHelper": + MessageLookupByLibrary.simpleMessage("Enter a new speed option to add"), + "playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage( + "Select Speed", + ), + "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( + "Enter the speed you want to set when playing for the first time", + ), + "playlistsMine": MessageLookupByLibrary.simpleMessage("My Playlists"), + "readLess": MessageLookupByLibrary.simpleMessage("Read Less"), + "readMore": MessageLookupByLibrary.simpleMessage("Read More"), + "refresh": MessageLookupByLibrary.simpleMessage("Refresh"), + "reset": MessageLookupByLibrary.simpleMessage("Reset"), + "resetAppSettings": MessageLookupByLibrary.simpleMessage( + "Reset App Settings", + ), + "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Reset the app settings to the default values", + ), + "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( + "Are you sure you want to reset the app settings?", + ), + "restore": MessageLookupByLibrary.simpleMessage("Restore"), + "restoreBackup": MessageLookupByLibrary.simpleMessage("Restore Backup"), + "restoreBackupHint": MessageLookupByLibrary.simpleMessage( + "Paste the backup here", + ), + "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage( + "Invalid backup", + ), + "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage( + "Settings restored", + ), + "restoreBackupValidator": MessageLookupByLibrary.simpleMessage( + "Please paste the backup here", + ), + "restoreDescription": MessageLookupByLibrary.simpleMessage( + "Restore the app settings from the backup", + ), + "resume": MessageLookupByLibrary.simpleMessage("Resume"), + "retry": MessageLookupByLibrary.simpleMessage("Retry"), + "settings": MessageLookupByLibrary.simpleMessage("Settings"), + "shakeAction": MessageLookupByLibrary.simpleMessage("Shake Action"), + "shakeActionDescription": MessageLookupByLibrary.simpleMessage( + "The action to perform when a shake is detected", + ), + "shakeActivationThreshold": MessageLookupByLibrary.simpleMessage( + "Shake Activation Threshold", + ), + "shakeActivationThresholdDescription": MessageLookupByLibrary.simpleMessage( + "The higher the threshold, the harder you need to shake", + ), + "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), + "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( + "Customize the shake detector settings", + ), + "shakeDetectorEnable": MessageLookupByLibrary.simpleMessage( + "Enable Shake Detection", + ), + "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( + "Enable shake detection to do various actions", + ), + "shakeDetectorSettings": MessageLookupByLibrary.simpleMessage( + "Shake Detector Settings", + ), + "shakeFeedback": MessageLookupByLibrary.simpleMessage("Shake Feedback"), + "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( + "The feedback to give when a shake is detected", + ), + "shakeSelectAction": MessageLookupByLibrary.simpleMessage( + "Select Shake Action", + ), + "shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage( + "Select Shake Activation Threshold", + ), + "shakeSelectActivationThresholdHelper": + MessageLookupByLibrary.simpleMessage( "Enter a number to set the threshold in m/s²", ), - "shakeSelectFeedback": MessageLookupByLibrary.simpleMessage( - "Select Shake Feedback", - ), - "themeMode": MessageLookupByLibrary.simpleMessage("Theme Mode"), - "themeModeDark": MessageLookupByLibrary.simpleMessage("Dark"), - "themeModeHighContrast": MessageLookupByLibrary.simpleMessage( - "High Contrast Mode", - ), - "themeModeHighContrastDescription": - MessageLookupByLibrary.simpleMessage( - "Increase the contrast between the background and the text", - ), - "themeModeLight": MessageLookupByLibrary.simpleMessage("Light"), - "themeModeSystem": MessageLookupByLibrary.simpleMessage("System"), - "themeSettings": MessageLookupByLibrary.simpleMessage("Theme Settings"), - "themeSettingsColors": MessageLookupByLibrary.simpleMessage( - "Material Theme from System", - ), - "themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage( - "Use Material You", - ), - "themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( - "Adaptive Theme on Item Page", - ), - "themeSettingsColorsBookDescription": - MessageLookupByLibrary.simpleMessage( - "Get fancy with the colors on the item page at the cost of some performance", - ), - "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( - "Adapt theme from currently playing item", - ), - "themeSettingsColorsCurrentDescription": - MessageLookupByLibrary.simpleMessage( + "shakeSelectFeedback": MessageLookupByLibrary.simpleMessage( + "Select Shake Feedback", + ), + "themeMode": MessageLookupByLibrary.simpleMessage("Theme Mode"), + "themeModeDark": MessageLookupByLibrary.simpleMessage("Dark"), + "themeModeHighContrast": MessageLookupByLibrary.simpleMessage( + "High Contrast Mode", + ), + "themeModeHighContrastDescription": MessageLookupByLibrary.simpleMessage( + "Increase the contrast between the background and the text", + ), + "themeModeLight": MessageLookupByLibrary.simpleMessage("Light"), + "themeModeSystem": MessageLookupByLibrary.simpleMessage("System"), + "themeSettings": MessageLookupByLibrary.simpleMessage("Theme Settings"), + "themeSettingsColors": MessageLookupByLibrary.simpleMessage( + "Material Theme from System", + ), + "themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage( + "Use Material You", + ), + "themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( + "Adaptive Theme on Item Page", + ), + "themeSettingsColorsBookDescription": MessageLookupByLibrary.simpleMessage( + "Get fancy with the colors on the item page at the cost of some performance", + ), + "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( + "Adapt theme from currently playing item", + ), + "themeSettingsColorsCurrentDescription": + MessageLookupByLibrary.simpleMessage( "Use the theme colors from the currently playing item for the app", ), - "themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( - "Use the system theme colors for the app", - ), - "themeSettingsDescription": MessageLookupByLibrary.simpleMessage( - "Customize the app theme", - ), - "timeSecond": m7, - "unknown": MessageLookupByLibrary.simpleMessage("Unknown"), - "webVersion": MessageLookupByLibrary.simpleMessage("Web Version"), - "yes": MessageLookupByLibrary.simpleMessage("Yes"), - "you": MessageLookupByLibrary.simpleMessage("You"), - "youTooltip": MessageLookupByLibrary.simpleMessage( - "Your Profile and Settings", - ), - }; + "themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( + "Use the system theme colors for the app", + ), + "themeSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Customize the app theme", + ), + "timeSecond": m7, + "unknown": MessageLookupByLibrary.simpleMessage("Unknown"), + "webVersion": MessageLookupByLibrary.simpleMessage("Web Version"), + "yes": MessageLookupByLibrary.simpleMessage("Yes"), + "you": MessageLookupByLibrary.simpleMessage("You"), + "youTooltip": MessageLookupByLibrary.simpleMessage( + "Your Profile and Settings", + ), + }; } diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index f212007..2cc62a5 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -38,348 +38,319 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "account": MessageLookupByLibrary.simpleMessage("账户"), - "accountAddNewServer": MessageLookupByLibrary.simpleMessage("添加新服务器"), - "accountAddUser": MessageLookupByLibrary.simpleMessage("添加用户"), - "accountAddUserDialog": m0, - "accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage( - "用户添加成功!切换?", - ), - "accountAddUserTooltip": MessageLookupByLibrary.simpleMessage("添加新服务器"), - "accountAnonymous": MessageLookupByLibrary.simpleMessage("匿名"), - "accountDeleteServer": MessageLookupByLibrary.simpleMessage("删除服务器"), - "accountInvalidURL": MessageLookupByLibrary.simpleMessage("无效网址"), - "accountManage": MessageLookupByLibrary.simpleMessage("帐户管理"), - "accountRegisteredServers": - MessageLookupByLibrary.simpleMessage("已注册服务器"), - "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( - "删除服务器和用户", - ), - "accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage( - "这将删除服务器 ", - ), - "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( - " 以及该应用程序中所有用户的登录信息。", - ), - "accountRemoveUserLogin": - MessageLookupByLibrary.simpleMessage("删除用户登录"), - "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( - "这将删除用户 ", - ), - "accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage( - " 的登录详细信息。", - ), - "accountServerURI": MessageLookupByLibrary.simpleMessage("服务器地址"), - "accountSwitch": MessageLookupByLibrary.simpleMessage("切换账户"), - "accountUsersCount": m1, - "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), - "appearance": MessageLookupByLibrary.simpleMessage("外观"), - "autoSleepTimerSettings": - MessageLookupByLibrary.simpleMessage("自动睡眠定时器设置"), - "autoTurnOnSleepTimer": - MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), - "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage("自动开启定时器"), - "autoTurnOnTimerAlways": - MessageLookupByLibrary.simpleMessage("始终自动开启定时器"), - "autoTurnOnTimerAlwaysDescription": - MessageLookupByLibrary.simpleMessage( - "总是打开睡眠定时器", - ), - "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( - "根据一天中的时间自动打开睡眠定时器", - ), - "autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage("从"), - "autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage( - "在指定时间打开睡眠定时器", - ), - "autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("直到"), - "autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage( - "在指定时间关闭睡眠定时器", - ), - "automaticallyDescription": MessageLookupByLibrary.simpleMessage( - "根据一天中的时间自动打开睡眠定时器", - ), - "backup": MessageLookupByLibrary.simpleMessage("备份"), - "backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"), - "bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"), - "bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"), - "bookAuthors": MessageLookupByLibrary.simpleMessage("作者"), - "bookDownloads": MessageLookupByLibrary.simpleMessage("下载"), - "bookGenres": MessageLookupByLibrary.simpleMessage("风格"), - "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("删节版"), - "bookMetadataLength": MessageLookupByLibrary.simpleMessage("持续时间"), - "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("发布年份"), - "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage("未删节版"), - "bookSeries": MessageLookupByLibrary.simpleMessage("系列"), - "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"), - "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"), - "cancel": MessageLookupByLibrary.simpleMessage("取消"), - "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), - "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( - "将应用程序设置复制到剪贴板", - ), - "copyToClipboardToast": - MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), - "delete": MessageLookupByLibrary.simpleMessage("删除"), - "deleteDialog": m2, - "deleted": m3, - "explore": MessageLookupByLibrary.simpleMessage("探索"), - "exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."), - "exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"), - "general": MessageLookupByLibrary.simpleMessage("通用"), - "help": MessageLookupByLibrary.simpleMessage("Help"), - "home": MessageLookupByLibrary.simpleMessage("首页"), - "homeBookContinueListening": - MessageLookupByLibrary.simpleMessage("继续收听"), - "homeBookContinueListeningDescription": - MessageLookupByLibrary.simpleMessage("继续收听书架上显示播放按钮"), - "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"), - "homeBookContinueSeriesDescription": - MessageLookupByLibrary.simpleMessage( - "继续系列书架上显示播放按钮", - ), - "homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"), - "homeBookListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), - "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( - "再听一遍书架上显示播放按钮", - ), - "homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage("最新作者"), - "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage("最近添加"), - "homeBookRecommended": MessageLookupByLibrary.simpleMessage("推荐"), - "homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), - "homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), - "homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"), - "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( - "自定义主页", - ), - "homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage( - "其他书架", - ), - "homePageSettingsOtherShelvesDescription": - MessageLookupByLibrary.simpleMessage("显示所有剩余书架上所有书籍的播放按钮"), - "homePageSettingsQuickPlay": - MessageLookupByLibrary.simpleMessage("继续播放"), - "homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"), - "language": MessageLookupByLibrary.simpleMessage("语言"), - "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), - "library": MessageLookupByLibrary.simpleMessage("媒体库"), - "libraryChange": MessageLookupByLibrary.simpleMessage("更改媒体库"), - "libraryEmpty": MessageLookupByLibrary.simpleMessage("没有可用的库。"), - "libraryLoadError": m4, - "librarySelect": MessageLookupByLibrary.simpleMessage("选择媒体库"), - "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage("切换媒体库"), - "libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"), - "loading": MessageLookupByLibrary.simpleMessage("加载中..."), - "loginLocal": MessageLookupByLibrary.simpleMessage("Local"), - "loginLogin": MessageLookupByLibrary.simpleMessage("登录"), - "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), - "loginPassword": MessageLookupByLibrary.simpleMessage("密码"), - "loginServerClick": MessageLookupByLibrary.simpleMessage("单击此处"), - "loginServerConnected": - MessageLookupByLibrary.simpleMessage("服务器已连接,请登录"), - "loginServerNo": MessageLookupByLibrary.simpleMessage("没有服务器? "), - "loginServerNoConnected": MessageLookupByLibrary.simpleMessage( - "请输入您的AudiobookShelf服务器的URL", - ), - "loginServerNot": m5, - "loginServerTo": MessageLookupByLibrary.simpleMessage(" 了解如何设置服务器。"), - "loginTitle": m6, - "loginToken": MessageLookupByLibrary.simpleMessage("Token"), - "loginUsername": MessageLookupByLibrary.simpleMessage("用户名"), - "logs": MessageLookupByLibrary.simpleMessage("日志"), - "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage("快退间隔"), - "nmpSettingsForward": MessageLookupByLibrary.simpleMessage("快进间隔"), - "nmpSettingsMediaControls": - MessageLookupByLibrary.simpleMessage("媒体控制"), - "nmpSettingsMediaControlsDescription": - MessageLookupByLibrary.simpleMessage( - "选择要显示的媒体控件", - ), - "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( - "在下面选择一个字段进行插入", - ), - "nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage( - "显示章节进度", - ), - "nmpSettingsShowChapterProgressDescription": - MessageLookupByLibrary.simpleMessage("而不是本书的整体进展"), - "nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage("副标题"), - "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( - "通知的副标题\n", - ), - "nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("主标题"), - "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( - "通知的标题\n", - ), - "no": MessageLookupByLibrary.simpleMessage("否"), - "notImplemented": MessageLookupByLibrary.simpleMessage("未实现"), - "notificationMediaPlayer": - MessageLookupByLibrary.simpleMessage("通知媒体播放器"), - "notificationMediaPlayerDescription": - MessageLookupByLibrary.simpleMessage( - "在通知中自定义媒体播放器", - ), - "ok": MessageLookupByLibrary.simpleMessage("确定"), - "pause": MessageLookupByLibrary.simpleMessage("暂停"), - "play": MessageLookupByLibrary.simpleMessage("播放"), - "playerSettings": MessageLookupByLibrary.simpleMessage("播放器设置"), - "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( - "剩余时间标记完成", - ), - "playerSettingsCompleteTimeDescriptionHead": - MessageLookupByLibrary.simpleMessage("当书中剩余时间少于 "), - "playerSettingsCompleteTimeDescriptionTail": - MessageLookupByLibrary.simpleMessage(" 时,标记完成"), - "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( - "自定义播放器设置", - ), - "playerSettingsDisplay": MessageLookupByLibrary.simpleMessage("显示设置"), - "playerSettingsDisplayChapterProgress": - MessageLookupByLibrary.simpleMessage("显示章节进度"), - "playerSettingsDisplayChapterProgressDescription": - MessageLookupByLibrary.simpleMessage("在播放器中显示当前章节的进度"), - "playerSettingsDisplayTotalProgress": - MessageLookupByLibrary.simpleMessage( - "显示总进度", - ), - "playerSettingsDisplayTotalProgressDescription": - MessageLookupByLibrary.simpleMessage("在播放器中显示当前书籍的总进度"), - "playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage( - "播放报告间隔", - ), - "playerSettingsPlaybackIntervalDescriptionHead": - MessageLookupByLibrary.simpleMessage("每 "), - "playerSettingsPlaybackIntervalDescriptionTail": - MessageLookupByLibrary.simpleMessage(" 向服务器报告一次进度"), - "playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage( - "回放报告", - ), - "playerSettingsPlaybackReportingIgnore": - MessageLookupByLibrary.simpleMessage("忽略播放位置小于"), - "playerSettingsPlaybackReportingMinimum": - MessageLookupByLibrary.simpleMessage("回放报告最小位置"), - "playerSettingsPlaybackReportingMinimumDescriptionHead": - MessageLookupByLibrary.simpleMessage("不要报告本书前 "), - "playerSettingsPlaybackReportingMinimumDescriptionTail": - MessageLookupByLibrary.simpleMessage(" 的播放"), - "playerSettingsRememberForEveryBook": - MessageLookupByLibrary.simpleMessage( - "记住每本书的播放器设置", - ), - "playerSettingsRememberForEveryBookDescription": - MessageLookupByLibrary.simpleMessage("每本书都会记住播放速度、音量等设置"), - "playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("播放速度"), - "playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage( - "默认播放速度", - ), - "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( - "播放速度选项", - ), - "playerSettingsSpeedOptionsSelect": - MessageLookupByLibrary.simpleMessage( - "播放速度选项", - ), - "playerSettingsSpeedOptionsSelectAdd": - MessageLookupByLibrary.simpleMessage( - "添加一个速度选项", - ), - "playerSettingsSpeedOptionsSelectAddHelper": - MessageLookupByLibrary.simpleMessage("输入一个新的速度选项"), - "playerSettingsSpeedSelect": - MessageLookupByLibrary.simpleMessage("选择播放速度"), - "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( - "输入默认的播放速度", - ), - "playlistsMine": MessageLookupByLibrary.simpleMessage("播放列表"), - "readLess": MessageLookupByLibrary.simpleMessage("折叠"), - "readMore": MessageLookupByLibrary.simpleMessage("展开"), - "refresh": MessageLookupByLibrary.simpleMessage("刷新"), - "reset": MessageLookupByLibrary.simpleMessage("重置"), - "resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"), - "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( - "将应用程序设置重置为默认值", - ), - "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( - "您确定要重置应用程序设置吗?", - ), - "restore": MessageLookupByLibrary.simpleMessage("恢复"), - "restoreBackup": MessageLookupByLibrary.simpleMessage("恢复备份"), - "restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"), - "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"), - "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"), - "restoreBackupValidator": - MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), - "restoreDescription": - MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), - "resume": MessageLookupByLibrary.simpleMessage("继续"), - "retry": MessageLookupByLibrary.simpleMessage("重试"), - "settings": MessageLookupByLibrary.simpleMessage("设置"), - "shakeAction": MessageLookupByLibrary.simpleMessage("抖动操作"), - "shakeActionDescription": MessageLookupByLibrary.simpleMessage( - "检测到抖动时要执行的操作", - ), - "shakeActivationThreshold": - MessageLookupByLibrary.simpleMessage("抖动激活阈值"), - "shakeActivationThresholdDescription": - MessageLookupByLibrary.simpleMessage( - "门槛越高,你就越难摇晃", - ), - "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), - "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( - "自定义抖动检测器设置", - ), - "shakeDetectorEnable": MessageLookupByLibrary.simpleMessage("启用抖动检测"), - "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( - "启用抖动检测以执行各种操作", - ), - "shakeDetectorSettings": - MessageLookupByLibrary.simpleMessage("抖动检测器设置"), - "shakeFeedback": MessageLookupByLibrary.simpleMessage("抖动反馈"), - "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( - "检测到抖动时给出的反馈", - ), - "shakeSelectAction": MessageLookupByLibrary.simpleMessage("选择抖动动作"), - "shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage( - "选择抖动激活阈值", - ), - "shakeSelectActivationThresholdHelper": - MessageLookupByLibrary.simpleMessage("输入一个数字以m/s²为单位设置阈值"), - "shakeSelectFeedback": MessageLookupByLibrary.simpleMessage("选择抖动反馈"), - "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), - "themeModeDark": MessageLookupByLibrary.simpleMessage("深色"), - "themeModeHighContrast": MessageLookupByLibrary.simpleMessage("高对比度模式"), - "themeModeHighContrastDescription": - MessageLookupByLibrary.simpleMessage( - "增加背景和文本之间的对比度", - ), - "themeModeLight": MessageLookupByLibrary.simpleMessage("浅色"), - "themeModeSystem": MessageLookupByLibrary.simpleMessage("跟随系统"), - "themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), - "themeSettingsColors": MessageLookupByLibrary.simpleMessage("主题色"), - "themeSettingsColorsAndroid": - MessageLookupByLibrary.simpleMessage("主题色"), - "themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( - "书籍详情页自适应主题", - ), - "themeSettingsColorsBookDescription": - MessageLookupByLibrary.simpleMessage( - "以牺牲一些性能为代价,对书籍详情页的颜色进行美化", - ), - "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( - "根据当前播放的书籍调整主题", - ), - "themeSettingsColorsCurrentDescription": - MessageLookupByLibrary.simpleMessage("使用当前播放书籍的主题颜色"), - "themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( - "使用应用程序的系统主题色", - ), - "themeSettingsDescription": - MessageLookupByLibrary.simpleMessage("自定义应用主题"), - "timeSecond": m7, - "unknown": MessageLookupByLibrary.simpleMessage("未知"), - "webVersion": MessageLookupByLibrary.simpleMessage("Web版本"), - "yes": MessageLookupByLibrary.simpleMessage("是"), - "you": MessageLookupByLibrary.simpleMessage("我的"), - "youTooltip": MessageLookupByLibrary.simpleMessage("您的个人资料和设置"), - }; + "account": MessageLookupByLibrary.simpleMessage("账户"), + "accountAddNewServer": MessageLookupByLibrary.simpleMessage("添加新服务器"), + "accountAddUser": MessageLookupByLibrary.simpleMessage("添加用户"), + "accountAddUserDialog": m0, + "accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage( + "用户添加成功!切换?", + ), + "accountAddUserTooltip": MessageLookupByLibrary.simpleMessage("添加新服务器"), + "accountAnonymous": MessageLookupByLibrary.simpleMessage("匿名"), + "accountDeleteServer": MessageLookupByLibrary.simpleMessage("删除服务器"), + "accountInvalidURL": MessageLookupByLibrary.simpleMessage("无效网址"), + "accountManage": MessageLookupByLibrary.simpleMessage("帐户管理"), + "accountRegisteredServers": MessageLookupByLibrary.simpleMessage("已注册服务器"), + "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( + "删除服务器和用户", + ), + "accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage( + "这将删除服务器 ", + ), + "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( + " 以及该应用程序中所有用户的登录信息。", + ), + "accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage("删除用户登录"), + "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( + "这将删除用户 ", + ), + "accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage( + " 的登录详细信息。", + ), + "accountServerURI": MessageLookupByLibrary.simpleMessage("服务器地址"), + "accountSwitch": MessageLookupByLibrary.simpleMessage("切换账户"), + "accountUsersCount": m1, + "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), + "appearance": MessageLookupByLibrary.simpleMessage("外观"), + "autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage("自动睡眠定时器设置"), + "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), + "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage("自动开启定时器"), + "autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage("始终自动开启定时器"), + "autoTurnOnTimerAlwaysDescription": MessageLookupByLibrary.simpleMessage( + "总是打开睡眠定时器", + ), + "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( + "根据一天中的时间自动打开睡眠定时器", + ), + "autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage("从"), + "autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage( + "在指定时间打开睡眠定时器", + ), + "autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("直到"), + "autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage( + "在指定时间关闭睡眠定时器", + ), + "automaticallyDescription": MessageLookupByLibrary.simpleMessage( + "根据一天中的时间自动打开睡眠定时器", + ), + "backup": MessageLookupByLibrary.simpleMessage("备份"), + "backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"), + "bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"), + "bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"), + "bookAuthors": MessageLookupByLibrary.simpleMessage("作者"), + "bookDownloads": MessageLookupByLibrary.simpleMessage("下载"), + "bookGenres": MessageLookupByLibrary.simpleMessage("风格"), + "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("删节版"), + "bookMetadataLength": MessageLookupByLibrary.simpleMessage("持续时间"), + "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("发布年份"), + "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage("未删节版"), + "bookSeries": MessageLookupByLibrary.simpleMessage("系列"), + "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"), + "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"), + "cancel": MessageLookupByLibrary.simpleMessage("取消"), + "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), + "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( + "将应用程序设置复制到剪贴板", + ), + "copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), + "delete": MessageLookupByLibrary.simpleMessage("删除"), + "deleteDialog": m2, + "deleted": m3, + "explore": MessageLookupByLibrary.simpleMessage("探索"), + "exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."), + "exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"), + "general": MessageLookupByLibrary.simpleMessage("通用"), + "help": MessageLookupByLibrary.simpleMessage("Help"), + "home": MessageLookupByLibrary.simpleMessage("首页"), + "homeBookContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), + "homeBookContinueListeningDescription": + MessageLookupByLibrary.simpleMessage("继续收听书架上显示播放按钮"), + "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"), + "homeBookContinueSeriesDescription": MessageLookupByLibrary.simpleMessage( + "继续系列书架上显示播放按钮", + ), + "homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"), + "homeBookListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), + "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( + "再听一遍书架上显示播放按钮", + ), + "homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage("最新作者"), + "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage("最近添加"), + "homeBookRecommended": MessageLookupByLibrary.simpleMessage("推荐"), + "homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), + "homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), + "homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"), + "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( + "自定义主页", + ), + "homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage( + "其他书架", + ), + "homePageSettingsOtherShelvesDescription": + MessageLookupByLibrary.simpleMessage("显示所有剩余书架上所有书籍的播放按钮"), + "homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage("继续播放"), + "homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"), + "language": MessageLookupByLibrary.simpleMessage("语言"), + "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), + "library": MessageLookupByLibrary.simpleMessage("媒体库"), + "libraryChange": MessageLookupByLibrary.simpleMessage("更改媒体库"), + "libraryEmpty": MessageLookupByLibrary.simpleMessage("没有可用的库。"), + "libraryLoadError": m4, + "librarySelect": MessageLookupByLibrary.simpleMessage("选择媒体库"), + "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage("切换媒体库"), + "libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"), + "loading": MessageLookupByLibrary.simpleMessage("加载中..."), + "loginLocal": MessageLookupByLibrary.simpleMessage("Local"), + "loginLogin": MessageLookupByLibrary.simpleMessage("登录"), + "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), + "loginPassword": MessageLookupByLibrary.simpleMessage("密码"), + "loginServerClick": MessageLookupByLibrary.simpleMessage("单击此处"), + "loginServerConnected": MessageLookupByLibrary.simpleMessage("服务器已连接,请登录"), + "loginServerNo": MessageLookupByLibrary.simpleMessage("没有服务器? "), + "loginServerNoConnected": MessageLookupByLibrary.simpleMessage( + "请输入您的AudiobookShelf服务器的URL", + ), + "loginServerNot": m5, + "loginServerTo": MessageLookupByLibrary.simpleMessage(" 了解如何设置服务器。"), + "loginTitle": m6, + "loginToken": MessageLookupByLibrary.simpleMessage("Token"), + "loginUsername": MessageLookupByLibrary.simpleMessage("用户名"), + "logs": MessageLookupByLibrary.simpleMessage("日志"), + "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage("快退间隔"), + "nmpSettingsForward": MessageLookupByLibrary.simpleMessage("快进间隔"), + "nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage("媒体控制"), + "nmpSettingsMediaControlsDescription": MessageLookupByLibrary.simpleMessage( + "选择要显示的媒体控件", + ), + "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( + "在下面选择一个字段进行插入", + ), + "nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage( + "显示章节进度", + ), + "nmpSettingsShowChapterProgressDescription": + MessageLookupByLibrary.simpleMessage("而不是本书的整体进展"), + "nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage("副标题"), + "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( + "通知的副标题\n", + ), + "nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("主标题"), + "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( + "通知的标题\n", + ), + "no": MessageLookupByLibrary.simpleMessage("否"), + "notImplemented": MessageLookupByLibrary.simpleMessage("未实现"), + "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"), + "notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( + "在通知中自定义媒体播放器", + ), + "ok": MessageLookupByLibrary.simpleMessage("确定"), + "pause": MessageLookupByLibrary.simpleMessage("暂停"), + "play": MessageLookupByLibrary.simpleMessage("播放"), + "playerSettings": MessageLookupByLibrary.simpleMessage("播放器设置"), + "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( + "剩余时间标记完成", + ), + "playerSettingsCompleteTimeDescriptionHead": + MessageLookupByLibrary.simpleMessage("当书中剩余时间少于 "), + "playerSettingsCompleteTimeDescriptionTail": + MessageLookupByLibrary.simpleMessage(" 时,标记完成"), + "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( + "自定义播放器设置", + ), + "playerSettingsDisplay": MessageLookupByLibrary.simpleMessage("显示设置"), + "playerSettingsDisplayChapterProgress": + MessageLookupByLibrary.simpleMessage("显示章节进度"), + "playerSettingsDisplayChapterProgressDescription": + MessageLookupByLibrary.simpleMessage("在播放器中显示当前章节的进度"), + "playerSettingsDisplayTotalProgress": MessageLookupByLibrary.simpleMessage( + "显示总进度", + ), + "playerSettingsDisplayTotalProgressDescription": + MessageLookupByLibrary.simpleMessage("在播放器中显示当前书籍的总进度"), + "playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage( + "播放报告间隔", + ), + "playerSettingsPlaybackIntervalDescriptionHead": + MessageLookupByLibrary.simpleMessage("每 "), + "playerSettingsPlaybackIntervalDescriptionTail": + MessageLookupByLibrary.simpleMessage(" 向服务器报告一次进度"), + "playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage( + "回放报告", + ), + "playerSettingsPlaybackReportingIgnore": + MessageLookupByLibrary.simpleMessage("忽略播放位置小于"), + "playerSettingsPlaybackReportingMinimum": + MessageLookupByLibrary.simpleMessage("回放报告最小位置"), + "playerSettingsPlaybackReportingMinimumDescriptionHead": + MessageLookupByLibrary.simpleMessage("不要报告本书前 "), + "playerSettingsPlaybackReportingMinimumDescriptionTail": + MessageLookupByLibrary.simpleMessage(" 的播放"), + "playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage( + "记住每本书的播放器设置", + ), + "playerSettingsRememberForEveryBookDescription": + MessageLookupByLibrary.simpleMessage("每本书都会记住播放速度、音量等设置"), + "playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("播放速度"), + "playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage( + "默认播放速度", + ), + "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( + "播放速度选项", + ), + "playerSettingsSpeedOptionsSelect": MessageLookupByLibrary.simpleMessage( + "播放速度选项", + ), + "playerSettingsSpeedOptionsSelectAdd": MessageLookupByLibrary.simpleMessage( + "添加一个速度选项", + ), + "playerSettingsSpeedOptionsSelectAddHelper": + MessageLookupByLibrary.simpleMessage("输入一个新的速度选项"), + "playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage("选择播放速度"), + "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( + "输入默认的播放速度", + ), + "playlistsMine": MessageLookupByLibrary.simpleMessage("播放列表"), + "readLess": MessageLookupByLibrary.simpleMessage("折叠"), + "readMore": MessageLookupByLibrary.simpleMessage("展开"), + "refresh": MessageLookupByLibrary.simpleMessage("刷新"), + "reset": MessageLookupByLibrary.simpleMessage("重置"), + "resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"), + "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( + "将应用程序设置重置为默认值", + ), + "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( + "您确定要重置应用程序设置吗?", + ), + "restore": MessageLookupByLibrary.simpleMessage("恢复"), + "restoreBackup": MessageLookupByLibrary.simpleMessage("恢复备份"), + "restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"), + "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"), + "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"), + "restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), + "restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), + "resume": MessageLookupByLibrary.simpleMessage("继续"), + "retry": MessageLookupByLibrary.simpleMessage("重试"), + "settings": MessageLookupByLibrary.simpleMessage("设置"), + "shakeAction": MessageLookupByLibrary.simpleMessage("抖动操作"), + "shakeActionDescription": MessageLookupByLibrary.simpleMessage( + "检测到抖动时要执行的操作", + ), + "shakeActivationThreshold": MessageLookupByLibrary.simpleMessage("抖动激活阈值"), + "shakeActivationThresholdDescription": MessageLookupByLibrary.simpleMessage( + "门槛越高,你就越难摇晃", + ), + "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), + "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( + "自定义抖动检测器设置", + ), + "shakeDetectorEnable": MessageLookupByLibrary.simpleMessage("启用抖动检测"), + "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( + "启用抖动检测以执行各种操作", + ), + "shakeDetectorSettings": MessageLookupByLibrary.simpleMessage("抖动检测器设置"), + "shakeFeedback": MessageLookupByLibrary.simpleMessage("抖动反馈"), + "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( + "检测到抖动时给出的反馈", + ), + "shakeSelectAction": MessageLookupByLibrary.simpleMessage("选择抖动动作"), + "shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage( + "选择抖动激活阈值", + ), + "shakeSelectActivationThresholdHelper": + MessageLookupByLibrary.simpleMessage("输入一个数字以m/s²为单位设置阈值"), + "shakeSelectFeedback": MessageLookupByLibrary.simpleMessage("选择抖动反馈"), + "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), + "themeModeDark": MessageLookupByLibrary.simpleMessage("深色"), + "themeModeHighContrast": MessageLookupByLibrary.simpleMessage("高对比度模式"), + "themeModeHighContrastDescription": MessageLookupByLibrary.simpleMessage( + "增加背景和文本之间的对比度", + ), + "themeModeLight": MessageLookupByLibrary.simpleMessage("浅色"), + "themeModeSystem": MessageLookupByLibrary.simpleMessage("跟随系统"), + "themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), + "themeSettingsColors": MessageLookupByLibrary.simpleMessage("主题色"), + "themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage("主题色"), + "themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( + "书籍详情页自适应主题", + ), + "themeSettingsColorsBookDescription": MessageLookupByLibrary.simpleMessage( + "以牺牲一些性能为代价,对书籍详情页的颜色进行美化", + ), + "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( + "根据当前播放的书籍调整主题", + ), + "themeSettingsColorsCurrentDescription": + MessageLookupByLibrary.simpleMessage("使用当前播放书籍的主题颜色"), + "themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( + "使用应用程序的系统主题色", + ), + "themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"), + "timeSecond": m7, + "unknown": MessageLookupByLibrary.simpleMessage("未知"), + "webVersion": MessageLookupByLibrary.simpleMessage("Web版本"), + "yes": MessageLookupByLibrary.simpleMessage("是"), + "you": MessageLookupByLibrary.simpleMessage("我的"), + "youTooltip": MessageLookupByLibrary.simpleMessage("您的个人资料和设置"), + }; } diff --git a/lib/main.dart b/lib/main.dart index 99a46f0..09daa8b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,24 +1,20 @@ +import 'dart:io'; + import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.dart'; -import 'package:vaani/features/downloads/providers/download_manager.dart'; import 'package:vaani/features/logging/core/logger.dart'; -import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; -import 'package:vaani/features/player/core/init.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; -import 'package:vaani/features/skip_start_end/skip_start_end_provider.dart'; -import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; +import 'package:vaani/framework.dart'; import 'package:vaani/generated/l10n.dart'; import 'package:vaani/globals.dart'; -import 'package:vaani/models/tray.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/shared/utils/utils.dart'; import 'package:vaani/theme/providers/system_theme_provider.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/theme.dart'; @@ -27,18 +23,8 @@ import 'package:window_manager/window_manager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // 初始化窗口管理器 - if (Utils.isDesktop()) { - await windowManager.ensureInitialized(); - final windowOptions = WindowOptions( - minimumSize: Size(1050, 700), - center: true, - skipTaskbar: false, - ); - await windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.setPreventClose(true); - }); - } + _runPlatformSpecificCode(); + // Configure the App Metadata await initialize(); @@ -49,16 +35,47 @@ void main() async { await initStorage(); // initialize audio player - await configurePlayer(); + // await configurePlayer(); // run the app runApp( const ProviderScope( - child: _EagerInitialization(child: TrayFramework(AbsApp())), + child: Framework( + // audioHandler: , + child: AbsApp(), + ), ), ); } +Future _runPlatformSpecificCode() async { + if (kIsWeb) return; + switch (Platform.operatingSystem) { + case 'android': + break; + case 'ios': + break; + case 'linux': + break; + case 'macos': + break; + case 'windows': + // 初始化窗口管理器 + await windowManager.ensureInitialized(); + final windowOptions = WindowOptions( + minimumSize: Size(1050, 700), + center: true, + skipTaskbar: false, + ); + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setPreventClose(true); + }); + break; + default: + break; + } +} + var routerConfig = const MyAppRouter().config; class AbsApp extends ConsumerWidget { @@ -172,29 +189,3 @@ class AbsApp extends ConsumerWidget { } } } - -// https://riverpod.dev/docs/essentials/eager_initialization -// Eagerly initialize providers by watching them. -class _EagerInitialization extends ConsumerWidget { - const _EagerInitialization({required this.child}); - final Widget child; - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Eagerly initialize providers by watching them. - // By using "watch", the provider will stay alive and not be disposed. - try { - ref.watch(simpleAudiobookPlayerProvider); - ref.watch(sleepTimerProvider); - ref.watch(playbackReporterProvider); - ref.watch(simpleDownloadManagerProvider); - ref.watch(shakeDetectorProvider); - ref.watch(skipStartEndProvider); - } catch (e) { - debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); - appLogger.severe(e.toString()); - } - - return child; - } -} diff --git a/lib/models/tray.dart b/lib/models/tray.dart index ea9f139..fe3c6d3 100644 --- a/lib/models/tray.dart +++ b/lib/models/tray.dart @@ -22,7 +22,6 @@ class _TrayFrameworkState extends ConsumerState windowManager.addListener(this); _init(); } - super.initState(); } diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 35ff3e6..a4e075b 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -80,9 +80,11 @@ class ScaffoldWithNavBar extends HookConsumerWidget { ? libraryIcon ?? item.activeIcon : item.activeIcon, ), - label: Text(isDestinationLibrary - ? currentLibrary?.name ?? item.name - : item.name), + label: Text( + isDestinationLibrary + ? currentLibrary?.name ?? item.name + : item.name, + ), // tooltip: item.tooltip, ); // if (isDestinationLibrary) { @@ -101,7 +103,6 @@ class ScaffoldWithNavBar extends HookConsumerWidget { }).toList(), selectedIndex: navigationShell.currentIndex, onDestinationSelected: (int index) { - print(index); _onTap(context, index, ref); }, ), @@ -116,64 +117,56 @@ class ScaffoldWithNavBar extends HookConsumerWidget { } Widget? buildNavBottom(BuildContext context, WidgetRef ref) { - final size = MediaQuery.of(context).size; - final playerProgress = ref.watch(playerHeightProvider); - final playerMaxHeight = size.height; - var percentExpandedMiniPlayer = (playerProgress - playerMinHeight) / - (playerMaxHeight - playerMinHeight); - // Clamp the value between 0 and 1 - percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0); - return percentExpandedMiniPlayer != 1 - ? Opacity( - // Opacity is interpolated from 1 to 0 when player is expanded - opacity: 1 - percentExpandedMiniPlayer, - child: NavigationBar( - elevation: 0.0, - height: bottomBarHeight * (1 - percentExpandedMiniPlayer), + // final size = MediaQuery.of(context).size; + // final playerProgress = ref.watch(playerHeightProvider); + // final playerMaxHeight = size.height; + // var percentExpandedMiniPlayer = (playerProgress - playerMinHeight) / + // (playerMaxHeight - playerMinHeight); + // // Clamp the value between 0 and 1 + // percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0); + return NavigationBar( + elevation: 0.0, + height: bottomBarHeight.toDouble(), - // TODO: get destinations from the navigationShell - // Here, the items of BottomNavigationBar are hard coded. In a real - // world scenario, the items would most likely be generated from the - // branches of the shell route, which can be fetched using - // `navigationShell.route.branches`. - destinations: _navigationItems(context).map((item) { - final isDestinationLibrary = item.name == S.of(context).library; - var currentLibrary = - ref.watch(currentLibraryProvider).valueOrNull; - final libraryIcon = AbsIcons.getIconByName( - currentLibrary?.icon, - ); - final destinationWidget = NavigationDestination( - icon: Icon( - isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, - ), - selectedIcon: Icon( - isDestinationLibrary - ? libraryIcon ?? item.activeIcon - : item.activeIcon, - ), - label: isDestinationLibrary - ? currentLibrary?.name ?? item.name - : item.name, - tooltip: item.tooltip, - ); - if (isDestinationLibrary) { - return GestureDetector( - onSecondaryTap: () => showLibrarySwitcher(context, ref), - onDoubleTap: () => showLibrarySwitcher(context, ref), - child: - destinationWidget, // Wrap the actual NavigationDestination - ); - } else { - // Return the unwrapped destination for other items - return destinationWidget; - } - }).toList(), - selectedIndex: navigationShell.currentIndex, - onDestinationSelected: (int index) => _onTap(context, index, ref), - ), - ) - : null; + // TODO: get destinations from the navigationShell + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + destinations: _navigationItems(context).map((item) { + final isDestinationLibrary = item.name == S.of(context).library; + var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull; + final libraryIcon = AbsIcons.getIconByName( + currentLibrary?.icon, + ); + final destinationWidget = NavigationDestination( + icon: Icon( + isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, + ), + selectedIcon: Icon( + isDestinationLibrary + ? libraryIcon ?? item.activeIcon + : item.activeIcon, + ), + label: isDestinationLibrary + ? currentLibrary?.name ?? item.name + : item.name, + tooltip: item.tooltip, + ); + if (isDestinationLibrary) { + return GestureDetector( + onSecondaryTap: () => showLibrarySwitcher(context, ref), + onDoubleTap: () => showLibrarySwitcher(context, ref), + child: destinationWidget, // Wrap the actual NavigationDestination + ); + } else { + // Return the unwrapped destination for other items + return destinationWidget; + } + }).toList(), + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: (int index) => _onTap(context, index, ref), + ); } List<_NavigationItem> _navigationItems(BuildContext context) { diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 4fad4be..c8a4411 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -11,7 +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/audiobook_player.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'; import 'package:vaani/settings/app_settings_provider.dart'; @@ -212,10 +212,12 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final me = ref.watch(meProvider); - final player = ref.watch(audiobookPlayerProvider); - final isCurrentBookSetInPlayer = - player.book?.libraryItemId == libraryItemId; - final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; + // 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 isCurrentBookSetInPlayer = session?.libraryItemId == libraryItemId; + final isPlayingThisBook = playerState.playing && isCurrentBookSetInPlayer; final userProgress = me.valueOrNull?.mediaProgress ?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId); @@ -285,19 +287,13 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { .withValues(alpha: 0.9), ), ), - onPressed: () async { - final book = - await ref.watch(libraryItemProvider(libraryItemId).future); - - libraryItemPlayButtonOnPressed( - ref: ref, - book: book.media.asBookExpanded, - userMediaProgress: userProgress, - ); - }, + onPressed: () => session?.libraryItemId == libraryItemId + ? ref.read(sessionProvider).load(libraryItemId, null) + : ref.read(playerProvider).togglePlayPause(), icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + libraryItemId, child: DynamicItemPlayIcon( + isLoading: sessionLoading, isBookCompleted: isBookCompleted, isPlayingThisBook: isPlayingThisBook, isCurrentBookSetInPlayer: isCurrentBookSetInPlayer, @@ -336,3 +332,30 @@ class BookCoverSkeleton extends StatelessWidget { ); } } + +class BookCoverWidget extends HookConsumerWidget { + const BookCoverWidget({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(sessionProvider).session; + if (session == null) { + return const BookCoverSkeleton(); + } + final itemBeingPlayed = + ref.watch(libraryItemProvider(session.libraryItemId)); + final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null + ? ref.watch( + coverImageProvider(itemBeingPlayed.valueOrNull!.id), + ) + : null; + return imageOfItemBeingPlayed?.valueOrNull != null + ? Image.memory( + imageOfItemBeingPlayed!.valueOrNull!, + fit: BoxFit.cover, + ) + : const BookCoverSkeleton(); + } +} diff --git a/pubspec.lock b/pubspec.lock index c7cefee..ec90a5c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -318,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" custom_lint: dependency: "direct dev" description: @@ -520,6 +528,14 @@ 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: @@ -762,15 +778,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.5" - just_audio_background: - dependency: "direct main" - description: - path: just_audio_background - ref: media-notification-config - resolved-ref: fce45f334f0838cb6f630548efb65fec40ff17b4 - url: "https://github.com/Dr-Blank/just_audio" - source: git - version: "0.0.1-beta.15" just_audio_media_kit: dependency: "direct main" description: @@ -795,14 +802,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" - just_audio_windows: - dependency: "direct main" - description: - name: just_audio_windows - sha256: b1ba5305d841c0e3883644e20fc11aaa23f28cfdd43ec20236d1e119a402ef29 - url: "https://pub.dev" - source: hosted - version: "0.2.2" leak_tracker: dependency: transitive description: @@ -915,6 +914,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + media_kit_libs_windows_audio: + dependency: "direct main" + description: + name: media_kit_libs_windows_audio + sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 + url: "https://pub.dev" + source: hosted + version: "1.0.9" menu_base: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8ac4939..3375f71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ">=3.3.4 <4.0.0" flutter: 3.32.0 -isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used +isar_version: &isar_version ^4.0.0-dev.14 # define the version to be used # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -42,7 +42,8 @@ dependencies: cached_network_image: ^3.3.1 coast: ^2.0.2 collection: ^1.18.0 - # cupertino_icons: ^1.0.6 + cupertino_icons: ^1.0.6 + flutter_platform_widgets: ^9.0.0 device_info_plus: ^11.3.3 duration_picker: ^1.2.0 dynamic_color: ^1.7.0 @@ -57,22 +58,22 @@ dependencies: # font_awesome_flutter: ^10.7.0 freezed_annotation: ^2.4.1 go_router: ^14.0.2 - hive: ^4.0.0-dev.2 - hooks_riverpod: ^2.5.1 - isar: ^4.0.0-dev.13 - isar_flutter_libs: ^4.0.0-dev.13 + hive: ^4.0.0-dev.2 + hooks_riverpod: ^2.5.1 + isar: ^4.0.0-dev.14 + isar_flutter_libs: ^4.0.0-dev.14 json_annotation: ^4.9.0 just_audio: ^0.10.5 - just_audio_background: - # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed - git: - url: https://github.com/Dr-Blank/just_audio - ref: media-notification-config - path: just_audio_background - just_audio_windows: ^0.2.2 + # just_audio_background: + # # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed + # git: + # url: https://github.com/Dr-Blank/just_audio + # ref: media-notification-config + # path: just_audio_background + # just_audio_windows: ^0.2.2 just_audio_media_kit: ^2.0.4 media_kit_libs_linux: any - # media_kit_libs_windows_audio: any + media_kit_libs_windows_audio: any list_wheel_scroll_view_nls: ^0.0.3 logging: ^1.2.0 logging_appenders: ^1.3.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index db5c691..4f18e92 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include @@ -21,8 +21,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); - JustAudioWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("JustAudioWindowsPlugin")); + MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index cc33937..ed05458 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color isar_flutter_libs - just_audio_windows + media_kit_libs_windows_audio permission_handler_windows screen_retriever_windows share_plus 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 2/2] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=B0=E6=92=AD?= =?UTF-8?q?=E6=94=BE=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(); }