Merge branch 'dev'

This commit is contained in:
rang 2025-11-22 16:33:35 +08:00
commit 4ac36b8f87
42 changed files with 2349 additions and 1412 deletions

View file

@ -14,4 +14,5 @@ class AppElementSizes {
static const double iconSizeLarge = 64.0; static const double iconSizeLarge = 64.0;
static const double barHeight = 3.0; static const double barHeight = 3.0;
static const double barHeightLarge = 5.0;
} }

View file

@ -12,17 +12,15 @@ import 'package:vaani/features/downloads/providers/download_manager.dart'
downloadManagerProvider, downloadManagerProvider,
isItemDownloadedProvider, isItemDownloadedProvider,
isItemDownloadingProvider, isItemDownloadingProvider,
itemDownloadProgressProvider, itemDownloadProgressProvider;
simpleDownloadManagerProvider;
import 'package:vaani/features/item_viewer/view/library_item_page.dart'; 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/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/generated/l10n.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.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/extensions/model_conversions.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
@ -302,7 +300,7 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null; final isBookPlaying = ref.watch(sessionProvider)?.libraryItemId == item.id;
return IconButton( return IconButton(
onPressed: () { onPressed: () {
@ -434,10 +432,14 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final session = ref.watch(sessionProvider);
final book = item.media.asBookExpanded; final book = item.media.asBookExpanded;
final player = ref.watch(audiobookPlayerProvider); final playerStatusNotifier = ref.watch(playerStatusProvider);
final isCurrentBookSetInPlayer = player.book == book; final isLoading = playerStatusNotifier.isLoading(book.libraryItemId);
final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; final isCurrentBookSetInPlayer =
session?.libraryItemId == book.libraryItemId;
final isPlayingThisBook =
playerStatusNotifier.isPlaying() && isCurrentBookSetInPlayer;
final userMediaProgress = item.userMediaProgress; final userMediaProgress = item.userMediaProgress;
final isBookCompleted = userMediaProgress?.isFinished ?? false; final isBookCompleted = userMediaProgress?.isFinished ?? false;
@ -464,14 +466,15 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
} }
return ElevatedButton.icon( return ElevatedButton.icon(
onPressed: () => libraryItemPlayButtonOnPressed( onPressed: () {
ref: ref, session?.libraryItemId == book.libraryItemId
book: book, ? ref.read(playerProvider).togglePlayPause()
userMediaProgress: userMediaProgress, : ref.read(sessionProvider.notifier).load(book.libraryItemId, null);
), },
icon: Hero( icon: Hero(
tag: HeroTagPrefixes.libraryItemPlayButton + book.libraryItemId, tag: HeroTagPrefixes.libraryItemPlayButton + book.libraryItemId,
child: DynamicItemPlayIcon( child: DynamicItemPlayIcon(
isLoading: isLoading,
isCurrentBookSetInPlayer: isCurrentBookSetInPlayer, isCurrentBookSetInPlayer: isCurrentBookSetInPlayer,
isPlayingThisBook: isPlayingThisBook, isPlayingThisBook: isPlayingThisBook,
isBookCompleted: isBookCompleted, isBookCompleted: isBookCompleted,
@ -493,87 +496,32 @@ class DynamicItemPlayIcon extends StatelessWidget {
required this.isCurrentBookSetInPlayer, required this.isCurrentBookSetInPlayer,
required this.isPlayingThisBook, required this.isPlayingThisBook,
required this.isBookCompleted, required this.isBookCompleted,
this.isLoading = false,
}); });
final bool isCurrentBookSetInPlayer; final bool isCurrentBookSetInPlayer;
final bool isPlayingThisBook; final bool isPlayingThisBook;
final bool isBookCompleted; final bool isBookCompleted;
final bool isLoading;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Icon( return isLoading
isCurrentBookSetInPlayer ? SizedBox(
? isPlayingThisBook // width: 20,
? Icons.pause_rounded // height: 20,
: Icons.play_arrow_rounded child: CircularProgressIndicator(
: isBookCompleted strokeWidth: 4,
? Icons.replay_rounded ),
: Icons.play_arrow_rounded, )
); : 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<void> 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<void>? 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();
}

View file

@ -0,0 +1,275 @@
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_session.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 AbsAudioHandler 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<StreamSubscription> _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 PlaybackSessionExpanded 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<void> tryReportPlayback(_) async {
_logger.fine(
'callback called when elapsed ${_stopwatch.elapsed}',
);
if (player.positionInBook >= _session.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<void> 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<void> markComplete() async {
// if (player.book == null) {
// throw NoAudiobookPlayingError();
// }
await authenticatedApi.me.createUpdateMediaProgress(
libraryItemId: _session.libraryItemId,
parameters: CreateUpdateProgressReqParams(
isFinished: true,
currentTime: player.positionInBook,
duration: _session.duration,
),
responseErrorHandler: _responseErrorHandler,
);
_logger.info('Marked complete for book: ${_session.libraryItemId}');
}
Future<void> 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<void> 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: _session.duration,
);
}
}
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';
}
}

View file

@ -5,7 +5,7 @@ library;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:just_audio/just_audio.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:logging/logging.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
@ -124,19 +124,19 @@ class AudiobookPlayer extends AudioPlayer {
// ); // );
return AudioSource.uri( return AudioSource.uri(
retrievedUri, retrievedUri,
tag: MediaItem( // tag: MediaItem(
// Specify a unique ID for each media item: // // Specify a unique ID for each media item:
id: book.libraryItemId + track.index.toString(), // id: book.libraryItemId + track.index.toString(),
// Metadata to display in the notification: // // Metadata to display in the notification:
title: appSettings.notificationSettings.primaryTitle // title: appSettings.notificationSettings.primaryTitle
.formatNotificationTitle(book), // .formatNotificationTitle(book),
album: appSettings.notificationSettings.secondaryTitle // album: appSettings.notificationSettings.secondaryTitle
.formatNotificationTitle(book), // .formatNotificationTitle(book),
artUri: artworkUri ?? // artUri: artworkUri ??
Uri.parse( // Uri.parse(
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', // '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
), // ),
), // ),
); );
}).toList(); }).toList();
await setAudioSources( await setAudioSources(

View file

@ -1,90 +1,99 @@
// my_audio_handler.dart // my_audio_handler.dart
import 'dart:io';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.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 // add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10); final offset = Duration(milliseconds: 10);
class HookAudioHandler extends BaseAudioHandler { class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final AudioPlayer _player = AudioPlayer(); final AudioPlayer _player = AudioPlayer();
final List<AudioSource> _playlist = []; // final List<AudioSource> _playlist = [];
final Ref ref; final Ref ref;
BookExpanded? _book; PlaybackSessionExpanded? _session;
/// the authentication token to access the [AudioTrack.contentUrl] final _currentChapterObject = BehaviorSubject<BookChapter?>.seeded(null);
final String token; AbsAudioHandler(this.ref) {
/// the base url for the audio files
final Uri baseUrl;
HookAudioHandler(this.ref, {required this.token, required this.baseUrl}) {
_setupAudioPlayer(); _setupAudioPlayer();
} }
void _setupAudioPlayer() { void _setupAudioPlayer() {
_player.setAudioSources(_playlist); final statusNotifier = ref.read(playerStatusProvider.notifier);
// //
// _player.positionStream.listen((position) {
// // _updateGlobalPosition(position);
// });
// //
// _player.currentIndexStream.listen((index) {
// if (index != null) {
// _onTrackChanged(index);
// }
// });
// //
_player.playbackEventStream.map(_transformEvent).pipe(playbackState); _player.playbackEventStream.map(_transformEvent).pipe(playbackState);
_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);
}
});
} }
// //
Future<void> setSourceAudiobook( Future<void> setSourceAudiobook(
BookExpanded audiobook, { PlaybackSessionExpanded playbackSession, {
Duration? initialPosition, required Uri baseUrl,
required String token,
List<Uri>? downloadedUris, List<Uri>? downloadedUris,
}) async { }) async {
_book = audiobook; _session = playbackSession;
//
_playlist.clear();
// //
for (final track in audiobook.tracks) { List<AudioSource> audioSources = [];
final audioSource = ProgressiveAudioSource( for (final track in playbackSession.audioTracks) {
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token), audioSources.add(
tag: MediaItem( AudioSource.uri(
id: '${audiobook.libraryItemId}${track.index}', _getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
title: track.title,
duration: track.duration,
), ),
); );
_playlist.add(audioSource);
} }
// playMediaItem(
final mediaItems = audiobook.tracks MediaItem(
.map( id: playbackSession.libraryItemId,
(track) => MediaItem( album: playbackSession.mediaMetadata.title,
id: '${audiobook.libraryItemId}${track.index}', title: playbackSession.displayTitle,
title: track.title, displaySubtitle: playbackSession.mediaType == MediaType.book
duration: track.duration, ? (playbackSession.mediaMetadata as BookMetadata).subtitle
), : null,
) duration: playbackSession.duration,
.toList(); artUri: Uri.parse(
'$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token',
queue.add(mediaItems); ),
),
);
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) { // if (initialPosition != null) {
await seekToPosition(initialPosition); // await seekInBook(initialPosition);
} // }
} }
// // // //
@ -97,51 +106,108 @@ class HookAudioHandler extends BaseAudioHandler {
// //
Future<void> skipToChapter(int chapterId) async { Future<void> 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, (ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'), orElse: () => throw Exception('Chapter not found'),
); );
await seekInBook(chapter.start + offset);
await seekToPosition(chapter.start + offset);
} }
Duration get positionInBook { PlaybackSessionExpanded? get session => _session;
if (_book != null && _player.currentIndex != null) {
return _book!.tracks[_player.currentIndex!].startOffset +
_player.position;
}
return Duration.zero;
}
// //
AudioTrack? get currentTrack { AudioTrack? get currentTrack {
if (_book == null) { if (_session == null || _player.currentIndex == null) {
return null; return null;
} }
return _book!.findTrackAtTime(positionInBook); return _session!.audioTracks[_player.currentIndex!];
} }
// //
BookChapter? get currentChapter { BookChapter? get currentChapter {
if (_book == null) { return _currentChapterObject.value;
return null; }
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<PlayerState> get playerStateStream => _player.playerStateStream;
Stream<Duration> get positionStream => _player.positionStream;
Stream<Duration> get positionStreamInBook {
return _player.positionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> 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<Duration> get bufferedPositionStreamInBook {
return _player.bufferedPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get positionStreamInChapter {
return _player.positionStream.distinct().map((position) {
return position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
});
}
Stream<BookChapter?> get chapterStream => _currentChapterObject.stream;
Future<void> togglePlayPause() async {
// check if book is set
if (_session == null) {
return Future.value();
} }
return _book!.findChapterAtTime(positionInBook); _player.playerState.playing ? await pause() : await play();
} }
// //
@override @override
Future<void> play() => _player.play(); Future<void> play() async {
await _player.play();
}
@override @override
Future<void> pause() => _player.pause(); Future<void> pause() async {
await _player.pause();
}
// / // /
@override @override
Future<void> skipToNext() async { Future<void> skipToNext() async {
if (_book == null) { if (_session == null) {
// 退 // 退
return _player.seekToNext(); return _player.seekToNext();
} }
@ -150,32 +216,28 @@ class HookAudioHandler extends BaseAudioHandler {
// 退 // 退
return _player.seekToNext(); return _player.seekToNext();
} }
final currentIndex = _book!.chapters.indexOf(chapter); final chapterIndex = _session!.chapters.indexOf(chapter);
if (currentIndex < _book!.chapters.length - 1) { if (chapterIndex < _session!.chapters.length - 1) {
// //
final nextChapter = _book!.chapters[currentIndex + 1]; final nextChapter = _session!.chapters[chapterIndex + 1];
await skipToChapter(nextChapter.id); await skipToChapter(nextChapter.id);
} }
} }
@override @override
Future<void> skipToPrevious() async { Future<void> skipToPrevious() async {
if (_book == null) {
return _player.seekToPrevious();
}
final chapter = currentChapter; final chapter = currentChapter;
if (chapter == null) { if (_session == null || chapter == null) {
return _player.seekToPrevious(); return _player.seekToPrevious();
} }
final currentIndex = _book!.chapters.indexOf(chapter); final currentIndex = _session!.chapters.indexOf(chapter);
if (currentIndex > 0) { if (currentIndex > 0) {
// //
final prevChapter = _book!.chapters[currentIndex - 1]; final prevChapter = _session!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id); await skipToChapter(prevChapter.id);
} else { } else {
// //
await seekToPosition(Duration.zero); await seekInBook(Duration.zero);
} }
} }
@ -188,30 +250,53 @@ class HookAudioHandler extends BaseAudioHandler {
if (track != null) { if (track != null) {
startOffset = track.startOffset; startOffset = track.startOffset;
} }
await seekToPosition(startOffset + position); await seekInBook(startOffset + position);
}
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
} }
// //
Future<void> seekToPosition(Duration globalPosition) async { Future<void> seekInBook(Duration globalPosition) async {
if (_book == null) return; if (_session == null) return;
// //
final track = _book!.findTrackAtTime(globalPosition); final track = _session!.findTrackAtTime(globalPosition);
final index = _book!.tracks.indexOf(track); final index = _session!.audioTracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset; Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack <= Duration.zero) { if (positionInTrack < Duration.zero) {
positionInTrack = offset; positionInTrack = Duration.zero;
} }
// //
await _player.seek(positionInTrack, index: index); await _player.seek(positionInTrack, index: index);
} }
AudioPlayer get player => _player;
PlaybackState _transformEvent(PlaybackEvent event) { PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState( return PlaybackState(
controls: [ controls: [
MediaControl.skipToPrevious, if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious,
MediaControl.rewind,
if (_player.playing) MediaControl.pause else MediaControl.play, 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.seek,
MediaAction.fastForward,
MediaAction.stop,
MediaAction.setSpeed,
if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext,
},
androidCompactActionIndices: const [1, 2, 3],
processingState: const { processingState: const {
ProcessingState.idle: AudioProcessingState.idle, ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading, ProcessingState.loading: AudioProcessingState.loading,
@ -222,9 +307,10 @@ class HookAudioHandler extends BaseAudioHandler {
AudioProcessingState.idle, AudioProcessingState.idle,
playing: _player.playing, playing: _player.playing,
updatePosition: _player.position, updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition, bufferedPosition: event.bufferedPosition,
speed: _player.speed, speed: _player.speed,
queueIndex: event.currentIndex, queueIndex: event.currentIndex,
captioningEnabled: false,
); );
} }
} }
@ -246,7 +332,7 @@ Uri _getUri(
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
} }
extension BookExpandedExtension on BookExpanded { extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded {
BookChapter findChapterAtTime(Duration position) { BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere( return chapters.firstWhere(
(element) { (element) {
@ -257,16 +343,23 @@ extension BookExpandedExtension on BookExpanded {
} }
AudioTrack findTrackAtTime(Duration position) { AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere( return audioTracks.firstWhere(
(element) { (element) {
return element.startOffset <= position && return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset; 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) { Duration getTrackStartOffset(int index) {
return tracks[index].startOffset; return audioTracks[index].startOffset;
} }
} }

View file

@ -1,62 +1,62 @@
import 'package:audio_service/audio_service.dart'; // import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart'; // import 'package:audio_session/audio_session.dart';
import 'package:just_audio_background/just_audio_background.dart' // import 'package:just_audio_background/just_audio_background.dart'
show JustAudioBackground, NotificationConfig; // show JustAudioBackground, NotificationConfig;
import 'package:just_audio_media_kit/just_audio_media_kit.dart' // import 'package:just_audio_media_kit/just_audio_media_kit.dart'
show JustAudioMediaKit; // show JustAudioMediaKit;
import 'package:vaani/settings/app_settings_provider.dart'; // import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/models/app_settings.dart'; // import 'package:vaani/settings/models/app_settings.dart';
Future<void> configurePlayer() async { // Future<void> configurePlayer() async {
// for playing audio on windows, linux // // for playing audio on windows, linux
JustAudioMediaKit.ensureInitialized(windows: false); // JustAudioMediaKit.ensureInitialized(windows: false);
// for configuring how this app will interact with other audio apps // // for configuring how this app will interact with other audio apps
final session = await AudioSession.instance; // final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech()); // await session.configure(const AudioSessionConfiguration.speech());
final appSettings = loadOrCreateAppSettings(); // final appSettings = loadOrCreateAppSettings();
// for playing audio in the background // // for playing audio in the background
await JustAudioBackground.init( // await JustAudioBackground.init(
androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio', // androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio',
androidNotificationChannelName: 'Audio playback', // androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: false, // androidNotificationOngoing: false,
androidStopForegroundOnPause: false, // androidStopForegroundOnPause: false,
androidNotificationChannelDescription: 'Audio playback in the background', // androidNotificationChannelDescription: 'Audio playback in the background',
androidNotificationIcon: 'drawable/ic_stat_logo', // androidNotificationIcon: 'drawable/ic_stat_logo',
rewindInterval: appSettings.notificationSettings.rewindInterval, // rewindInterval: appSettings.notificationSettings.rewindInterval,
fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, // fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
androidShowNotificationBadge: false, // androidShowNotificationBadge: false,
notificationConfigBuilder: (state) { // notificationConfigBuilder: (state) {
final controls = [ // final controls = [
if (appSettings.notificationSettings.mediaControls // if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.skipToPreviousChapter) && // .contains(NotificationMediaControl.skipToPreviousChapter) &&
state.hasPrevious) // state.hasPrevious)
MediaControl.skipToPrevious, // MediaControl.skipToPrevious,
if (appSettings.notificationSettings.mediaControls // if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.rewind)) // .contains(NotificationMediaControl.rewind))
MediaControl.rewind, // MediaControl.rewind,
if (state.playing) MediaControl.pause else MediaControl.play, // if (state.playing) MediaControl.pause else MediaControl.play,
if (appSettings.notificationSettings.mediaControls // if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.fastForward)) // .contains(NotificationMediaControl.fastForward))
MediaControl.fastForward, // MediaControl.fastForward,
if (appSettings.notificationSettings.mediaControls // if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.skipToNextChapter) && // .contains(NotificationMediaControl.skipToNextChapter) &&
state.hasNext) // state.hasNext)
MediaControl.skipToNext, // MediaControl.skipToNext,
if (appSettings.notificationSettings.mediaControls // if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.stop)) // .contains(NotificationMediaControl.stop))
MediaControl.stop, // MediaControl.stop,
]; // ];
return NotificationConfig( // return NotificationConfig(
controls: controls, // controls: controls,
systemActions: const { // systemActions: const {
MediaAction.seek, // MediaAction.seek,
MediaAction.seekForward, // MediaAction.seekForward,
MediaAction.seekBackward, // MediaAction.seekBackward,
}, // },
); // );
}, // },
); // );
} // }

View file

@ -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,
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.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/api/api_provider.dart';
import 'package:vaani/features/player/core/audiobook_player.dart' as core; import 'package:vaani/features/player/core/audiobook_player.dart' as core;
@ -38,9 +39,9 @@ class AudiobookPlayer extends _$AudiobookPlayer {
ref.onDispose(player.dispose); ref.onDispose(player.dispose);
// bind notify listeners to the player // bind notify listeners to the player
player.playerStateStream.listen((_) { // player.playerStateStream.listen((_) {
ref.notifyListeners(); // ref.notifyListeners();
}); // });
_logger.finer('created player'); _logger.finer('created player');
@ -51,6 +52,13 @@ class AudiobookPlayer extends _$AudiobookPlayer {
await state.setSpeed(speed); await state.setSpeed(speed);
ref.notifyListeners(); ref.notifyListeners();
} }
Future<void> setSourceAudiobook({
required shelfsdk.BookExpanded book,
shelfsdk.MediaProgress? userMediaProgress,
}) async {
ref.notifyListeners();
}
} }
@riverpod @riverpod

View file

@ -43,7 +43,7 @@ final simpleAudiobookPlayerProvider =
); );
typedef _$SimpleAudiobookPlayer = Notifier<core.AudiobookPlayer>; typedef _$SimpleAudiobookPlayer = Notifier<core.AudiobookPlayer>;
String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0'; String _$audiobookPlayerHash() => r'04448247e79c5d60b9fd6f98eeeb865f1e8d0ff8';
/// See also [AudiobookPlayer]. /// See also [AudiobookPlayer].
@ProviderFor(AudiobookPlayer) @ProviderFor(AudiobookPlayer)

View file

@ -12,8 +12,8 @@ final _logger = Logger('CurrentlyPlayingProvider');
@riverpod @riverpod
BookExpanded? currentlyPlayingBook(Ref ref) { BookExpanded? currentlyPlayingBook(Ref ref) {
try { try {
final player = ref.watch(audiobookPlayerProvider); final book = ref.watch(simpleAudiobookPlayerProvider.select((v) => v.book));
return player.book; return book;
} catch (e) { } catch (e) {
_logger.warning('Error getting currently playing book: $e'); _logger.warning('Error getting currently playing book: $e');
return null; return null;

View file

@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart';
// ************************************************************************** // **************************************************************************
String _$currentlyPlayingBookHash() => String _$currentlyPlayingBookHash() =>
r'e4258694c8f0d1e89651b330fae0f672ca13a484'; r'f2c47028340d253be9440dc29f835328ff30c0e6';
/// See also [currentlyPlayingBook]. /// See also [currentlyPlayingBook].
@ProviderFor(currentlyPlayingBook) @ProviderFor(currentlyPlayingBook)

View file

@ -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);
}
}

View file

@ -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<PlayerStatus, core.PlayerStatus>.internal(
PlayerStatus.new,
name: r'playerStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$playerStatusHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PlayerStatus = Notifier<core.PlayerStatus>;
// 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

View file

@ -0,0 +1,184 @@
import 'package:audio_service/audio_service.dart';
import 'package:http/http.dart' as http;
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/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';
@Riverpod(keepAlive: true)
Future<AbsAudioHandler> 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<void> load(String id, String? episodeId) async {
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,
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;
state = playBack;
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,
),
]);
}
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 CurrentChapter extends _$CurrentChapter {
@override
core.BookChapter? build() {
final player = ref.watch(playerProvider);
player.chapterStream.distinct().listen((chapter) {
update(chapter);
});
return player.currentChapter;
}
void update(core.BookChapter? chapter) {
if (state != chapter) {
state = chapter;
}
}
}
@Riverpod(keepAlive: true)
class PlaybackReporter extends _$PlaybackReporter {
@override
Future<core.PlaybackReporter?> 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);
final reporter = core.PlaybackReporter(
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';
}
}

View file

@ -0,0 +1,88 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'session_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$audioHandlerInitHash() => r'5677b2267f472b667ce7a63cc5c91c4320d630e8';
/// See also [audioHandlerInit].
@ProviderFor(audioHandlerInit)
final audioHandlerInitProvider = FutureProvider<AbsAudioHandler>.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<AbsAudioHandler>;
String _$playerHash() => r'9599b094cdd9eca614c27ec5bdf2d5259d20ac5f';
/// See also [Player].
@ProviderFor(Player)
final playerProvider = NotifierProvider<Player, AbsAudioHandler>.internal(
Player.new,
name: r'playerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$playerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Player = Notifier<AbsAudioHandler>;
String _$sessionHash() => r'ce9405cc0b8247014924a005fa60fbc3bf92d5c6';
/// See also [Session].
@ProviderFor(Session)
final sessionProvider =
NotifierProvider<Session, core.PlaybackSessionExpanded?>.internal(
Session.new,
name: r'sessionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Session = Notifier<core.PlaybackSessionExpanded?>;
String _$currentChapterHash() => r'0be02fbc4f65b30da4ce18964ee457a18c4d1073';
/// See also [CurrentChapter].
@ProviderFor(CurrentChapter)
final currentChapterProvider =
NotifierProvider<CurrentChapter, core.BookChapter?>.internal(
CurrentChapter.new,
name: r'currentChapterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentChapterHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CurrentChapter = Notifier<core.BookChapter?>;
String _$playbackReporterHash() => r'b9a2197a12e83f9760bd61ae2a1ad8dff82042e9';
/// See also [PlaybackReporter].
@ProviderFor(PlaybackReporter)
final playbackReporterProvider =
AsyncNotifierProvider<PlaybackReporter, core.PlaybackReporter?>.internal(
PlaybackReporter.new,
name: r'playbackReporterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$playbackReporterHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PlaybackReporter = AsyncNotifier<core.PlaybackReporter?>;
// 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

View file

@ -3,13 +3,11 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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_player_pause_button.dart';
import 'package:vaani/features/player/view/widgets/player_progress_bar.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/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/shared/widgets/not_implemented.dart'; import 'package:vaani/shared/widgets/not_implemented.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
@ -28,32 +26,20 @@ class PlayerExpanded extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final session = ref.watch(sessionProvider);
if (session == null) {
return SizedBox.shrink();
}
/// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer] /// 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% /// however, some properties need to start later than 0% and end before 100%
final currentBook = ref.watch(currentlyPlayingBookProvider); final currentChapter = ref.watch(currentChapterProvider);
if (currentBook == null) { // final currentBookMetadata = ref.watch(currentBookMetadataProvider);
return const SizedBox.shrink();
}
final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
// max height of the player is the height of the screen // max height of the player is the height of the screen
final playerMaxHeight = MediaQuery.of(context).size.height; final playerMaxHeight = MediaQuery.of(context).size.height;
final availWidth = MediaQuery.of(context).size.width; final availWidth = MediaQuery.of(context).size.width;
// the image width when the player is expanded // the image width when the player is expanded
final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9); 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
@ -104,7 +90,7 @@ class PlayerExpanded extends HookConsumerWidget {
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular, AppElementSizes.borderRadiusRegular,
), ),
child: imgWidget, child: BookCoverWidget(),
), ),
), ),
), ),
@ -133,8 +119,8 @@ class PlayerExpanded extends HookConsumerWidget {
padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular), padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular),
child: Text( child: Text(
[ [
currentBookMetadata?.title ?? '', session.displayTitle,
currentBookMetadata?.authorName ?? '', session.displayAuthor,
].join(' - '), ].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context) color: Theme.of(context)
@ -162,16 +148,14 @@ class PlayerExpanded extends HookConsumerWidget {
), ),
), ),
Expanded( SizedBox(
child: SizedBox( width: imageSize,
width: imageSize, child: Padding(
child: Padding( padding: EdgeInsets.only(
padding: EdgeInsets.only( left: AppElementSizes.paddingRegular,
left: AppElementSizes.paddingRegular, right: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular,
),
child: const AudiobookProgressBar(),
), ),
child: const AudiobookProgressBar(),
), ),
), ),

View file

@ -1,13 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.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:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/constants/sizes.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/session_provider.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
@ -20,25 +17,11 @@ class PlayerMinimized extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentBook = ref.watch(currentlyPlayingBookProvider); final session = ref.watch(sessionProvider);
if (currentBook == null) { if (session == null) {
return const SizedBox.shrink(); return SizedBox.shrink();
} }
final itemBeingPlayed = final currentChapter = ref.watch(currentChapterProvider);
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);
return PlayerMinimizedFramework( return PlayerMinimizedFramework(
children: [ children: [
@ -51,7 +34,7 @@ class PlayerMinimized extends HookConsumerWidget {
context.pushNamed( context.pushNamed(
Routes.libraryItem.name, Routes.libraryItem.name,
pathParameters: { pathParameters: {
Routes.libraryItem.pathParamName!: currentBook.libraryItemId, Routes.libraryItem.pathParamName!: session.libraryItemId,
}, },
); );
}, },
@ -59,7 +42,7 @@ class PlayerMinimized extends HookConsumerWidget {
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: playerMinimizedHeight, maxWidth: playerMinimizedHeight,
), ),
child: imgWidget, child: BookCoverWidget(),
), ),
), ),
), ),
@ -75,15 +58,15 @@ class PlayerMinimized extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// AutoScrollText( // AutoScrollText(
Text( PlatformText(
'${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}', '${session.displayTitle} - ${currentChapter?.title ?? ''}',
maxLines: 1, overflow: TextOverflow.ellipsis, maxLines: 1, overflow: TextOverflow.ellipsis,
// velocity: // velocity:
// const Velocity(pixelsPerSecond: Offset(16, 0)), // const Velocity(pixelsPerSecond: Offset(16, 0)),
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
Text( PlatformText(
bookMetaExpanded?.authorName ?? '', session.displayAuthor,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith( style: Theme.of(context).textTheme.bodyMedium!.copyWith(
@ -101,7 +84,7 @@ class PlayerMinimized extends HookConsumerWidget {
// rewind button // rewind button
Padding( Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsets.only(left: 8),
child: IconButton( child: PlatformIconButton(
icon: const Icon( icon: const Icon(
Icons.replay_30, Icons.replay_30,
size: AppElementSizes.iconSizeSmall, size: AppElementSizes.iconSizeSmall,
@ -127,9 +110,9 @@ class PlayerMinimizedFramework extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
final progress = final progress =
useStream(player.positionStream, initialData: Duration.zero); useStream(player.positionStreamInChapter, initialData: Duration.zero);
return GestureDetector( return GestureDetector(
onTap: () => context.pushNamed(Routes.player.name), onTap: () => context.pushNamed(Routes.player.name),
child: Container( child: Container(
@ -144,12 +127,10 @@ class PlayerMinimizedFramework extends HookConsumerWidget {
SizedBox( SizedBox(
height: AppElementSizes.barHeight, height: AppElementSizes.barHeight,
child: LinearProgressIndicator( child: LinearProgressIndicator(
// value: (progress.data ?? Duration.zero).inSeconds /
// player.book!.duration.inSeconds,
value: (progress.data ?? Duration.zero).inSeconds / value: (progress.data ?? Duration.zero).inSeconds /
(player.duration?.inSeconds ?? 1), (player.chapterDuration?.inSeconds ?? 1),
color: Theme.of(context).colorScheme.onPrimaryContainer, // color: Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor: Theme.of(context).colorScheme.primaryContainer, // backgroundColor: Theme.of(context).colorScheme.primaryContainer,
), ),
), ),
], ],

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.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 { class AudiobookPlayerSeekButton extends HookConsumerWidget {
const AudiobookPlayerSeekButton({ const AudiobookPlayerSeekButton({
@ -14,7 +14,7 @@ class AudiobookPlayerSeekButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
return IconButton( return IconButton(
icon: Icon( icon: Icon(
isForward ? Icons.forward_30 : Icons.replay_30, isForward ? Icons.forward_30 : Icons.replay_30,

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.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 { class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton({ const AudiobookPlayerSeekChapterButton({
@ -14,63 +14,26 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
// // 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,
// );
// }
return IconButton( return IconButton(
icon: Icon( icon: Icon(
isForward ? Icons.skip_next : Icons.skip_previous, isForward ? Icons.skip_next : Icons.skip_previous,
size: AppElementSizes.iconSizeSmall, size: AppElementSizes.iconSizeSmall,
), ),
onPressed: () { onPressed: () {
if (player.book == null) { if (player.session == null) {
return; return;
} }
// if chapter does not exist, go to the start or end of the book // if chapter does not exist, go to the start or end of the book
if (player.currentChapter == null) { if (player.currentChapter == null) {
player.seekInBook(isForward ? player.book!.duration : Duration.zero); player
.seekInBook(isForward ? player.session!.duration : Duration.zero);
return; return;
} }
if (isForward) { if (isForward) {
player.seekToNext(); player.skipToNext();
} else { } else {
player.seekToPrevious(); player.skipToPrevious();
} }
}, },
); );

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart' import 'package:vaani/features/player/providers/session_provider.dart';
show audiobookPlayerProvider;
import 'package:vaani/features/player/providers/currently_playing_provider.dart'
show currentPlayingChapterProvider, currentlyPlayingBookProvider;
import 'package:vaani/features/player/view/player_expanded.dart' import 'package:vaani/features/player/view/player_expanded.dart'
show pendingPlayerModals; show pendingPlayerModals;
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; 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/globals.dart';
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration; import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
import 'package:vaani/shared/extensions/duration_format.dart' import 'package:vaani/shared/extensions/duration_format.dart'
@ -22,14 +20,14 @@ class ChapterSelectionButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Tooltip( return Tooltip(
message: 'Chapters', message: S.of(context).chapters,
child: IconButton( child: IconButton(
icon: const Icon(Icons.menu_book_rounded), icon: const Icon(Icons.menu_book_rounded),
onPressed: () async { onPressed: () async {
pendingPlayerModals++; pendingPlayerModals++;
await showModalBottomSheet<bool>( await showModalBottomSheet<bool>(
context: context, context: context,
barrierLabel: 'Select Chapter', barrierLabel: S.of(context).chapterSelect,
constraints: BoxConstraints( constraints: BoxConstraints(
// 40% of the screen height // 40% of the screen height
maxHeight: MediaQuery.of(context).size.height * 0.4, maxHeight: MediaQuery.of(context).size.height * 0.4,
@ -55,9 +53,9 @@ class ChapterSelectionModal extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentChapter = ref.watch(currentPlayingChapterProvider); final session = ref.watch(sessionProvider);
final currentBook = ref.watch(currentlyPlayingBookProvider); final currentChapter = ref.watch(currentChapterProvider);
final notifier = ref.watch(audiobookPlayerProvider);
final currentChapterIndex = currentChapter?.id; final currentChapterIndex = currentChapter?.id;
final chapterKey = GlobalKey(); final chapterKey = GlobalKey();
scrollToCurrentChapter() async { scrollToCurrentChapter() async {
@ -77,7 +75,7 @@ class ChapterSelectionModal extends HookConsumerWidget {
children: [ children: [
ListTile( ListTile(
title: Text( 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 // scroll to current chapter after opening the dialog
@ -85,10 +83,10 @@ class ChapterSelectionModal extends HookConsumerWidget {
child: Scrollbar( child: Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
primary: true, primary: true,
child: currentBook?.chapters == null child: session?.chapters == null
? const Text('No chapters found') ? Text(S.of(context).chapterNotFound)
: Column( : Column(
children: currentBook!.chapters.map( children: session!.chapters.map(
(chapter) { (chapter) {
final isCurrent = currentChapterIndex == chapter.id; final isCurrent = currentChapterIndex == chapter.id;
final isPlayed = currentChapterIndex != null && final isPlayed = currentChapterIndex != null &&
@ -117,9 +115,9 @@ class ChapterSelectionModal extends HookConsumerWidget {
key: isCurrent ? chapterKey : null, key: isCurrent ? chapterKey : null,
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
// notifier.seekInBook(chapter.start + 90.ms); ref
notifier.skipToChapter(chapter.id); .read(playerProvider)
notifier.play(); .skipToChapter(chapter.id);
}, },
); );
}, },

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:vaani/features/player/core/player_status.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/session_provider.dart';
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({ const AudiobookPlayerPlayPauseButton({
@ -15,42 +14,42 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
final double iconSize; final double iconSize;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final playerStatus =
final playing = ref.watch(isPlayerPlayingProvider); ref.watch(playerStatusProvider.select((v) => v.playStatus));
final playPauseController = useAnimationController(
duration: const Duration(milliseconds: 200), return PlatformIconButton(
initialValue: 1, icon: _getIcon(playerStatus, context),
onPressed: () => _actionButtonPressed(playerStatus, ref),
); );
if (playing) { }
playPauseController.forward();
} else { Widget _getIcon(PlayStatus playerStatus, BuildContext context) {
playPauseController.reverse(); 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 (player.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(),
};
} }
} }

View file

@ -1,10 +1,9 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/providers/audiobook_player.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';
class AudiobookChapterProgressBar extends HookConsumerWidget { class AudiobookChapterProgressBar extends HookConsumerWidget {
const AudiobookChapterProgressBar({ const AudiobookChapterProgressBar({
@ -13,8 +12,8 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider); final currentChapter = ref.watch(currentChapterProvider);
final position = useStream( final position = useStream(
player.positionStreamInBook, player.positionStreamInBook,
initialData: const Duration(seconds: 0), initialData: const Duration(seconds: 0),
@ -38,7 +37,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
progress: progress:
currentChapterProgress ?? position.data ?? const Duration(seconds: 0), currentChapterProgress ?? position.data ?? const Duration(seconds: 0),
total: currentChapter == null total: currentChapter == null
? player.book?.duration ?? const Duration(seconds: 0) ? player.session?.duration ?? const Duration(seconds: 0)
: currentChapter.end - currentChapter.start, : currentChapter.end - currentChapter.start,
// ! TODO add onSeek // ! TODO add onSeek
onSeek: (duration) { onSeek: (duration) {
@ -64,19 +63,19 @@ class AudiobookProgressBar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
final position = useStream( final position = useStream(
player.slowPositionStreamInBook, player.slowPositionStreamInBook,
initialData: const Duration(seconds: 0), initialData: const Duration(seconds: 0),
); );
return ProgressBar( return SizedBox(
progress: position.data ?? const Duration(seconds: 0), height: AppElementSizes.barHeightLarge,
total: player.book?.duration ?? const Duration(seconds: 0), child: LinearProgressIndicator(
thumbRadius: 8, value: (position.data ?? const Duration(seconds: 0)).inSeconds /
bufferedBarColor: Theme.of(context).colorScheme.secondary, (player.session?.duration ?? const Duration(seconds: 0)).inSeconds,
timeLabelType: TimeLabelType.remainingTime, borderRadius: BorderRadiusGeometry.all(Radius.circular(10)),
timeLabelLocation: TimeLabelLocation.below, ),
); );
} }
} }

View file

@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.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/features/player/view/player_expanded.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/view/notification_settings_page.dart'; import 'package:vaani/settings/view/notification_settings_page.dart';
class SkipChapterStartEndButton extends HookConsumerWidget { class SkipChapterStartEndButton extends HookConsumerWidget {
@ -11,15 +14,16 @@ class SkipChapterStartEndButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Tooltip( return Tooltip(
message: "跳过片头片尾", message: S.of(context).chapterSkip,
child: IconButton( 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 { onPressed: () async {
// show toast // show toast
pendingPlayerModals++; pendingPlayerModals++;
await showModalBottomSheet<bool>( await showModalBottomSheet<bool>(
context: context, context: context,
barrierLabel: '跳过片头片尾', barrierLabel: S.of(context).chapterSkip,
constraints: BoxConstraints( constraints: BoxConstraints(
// 40% of the screen height // 40% of the screen height
maxHeight: MediaQuery.of(context).size.height * 0.4, maxHeight: MediaQuery.of(context).size.height * 0.4,
@ -43,15 +47,16 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final session = ref.watch(sessionProvider);
final bookId = player.book?.libraryItemId ?? '_'; final bookId = session?.libraryItemId ?? '_';
final bookSettings = ref.watch(bookSettingsProvider(bookId)); final bookSettings = ref.watch(bookSettingsProvider(bookId));
return Scaffold( return Scaffold(
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
title: Text( title: Text(
'跳过片头 ${bookSettings.playerSettings.skipChapterStart.inSeconds}s'), '${S.of(context).chapterSkipOpen}${bookSettings.playerSettings.skipChapterStart.inSeconds}s',
),
), ),
Expanded( Expanded(
child: TimeIntervalSlider( child: TimeIntervalSlider(
@ -75,7 +80,8 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget {
), ),
ListTile( ListTile(
title: Text( title: Text(
'跳过片尾 ${bookSettings.playerSettings.skipChapterEnd.inSeconds}s'), '${S.of(context).chapterSkipEnd}${bookSettings.playerSettings.skipChapterEnd.inSeconds}s',
),
), ),
Expanded( Expanded(
child: TimeIntervalSlider( child: TimeIntervalSlider(

View file

@ -2,8 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart' import 'package:vaani/features/player/providers/session_provider.dart';
show simpleAudiobookPlayerProvider;
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
show sleepTimerProvider; show sleepTimerProvider;
import 'package:vaani/settings/app_settings_provider.dart' 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 // 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) { player.playerStateStream.listen((event) {
if (event.processingState == ProcessingState.idle && wasPlayerLoaded) { if (event.processingState == ProcessingState.idle && wasPlayerLoaded) {
_logger.config('Player is now not loaded, invalidating'); _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'); _logger.config('No book is loaded, disabling shake detection');
wasPlayerLoaded = false; wasPlayerLoaded = false;
return null; return null;
@ -87,8 +86,8 @@ class ShakeDetector extends _$ShakeDetector {
ShakeAction shakeAction, { ShakeAction shakeAction, {
required Ref ref, required Ref ref,
}) { }) {
final player = ref.read(simpleAudiobookPlayerProvider); final player = ref.read(playerProvider);
if (player.book == null && shakeAction.isPlaybackManagementEnabled) { if (player.session == null && shakeAction.isPlaybackManagementEnabled) {
_logger.warning('No book is loaded'); _logger.warning('No book is loaded');
return false; return false;
} }
@ -104,19 +103,19 @@ class ShakeDetector extends _$ShakeDetector {
return true; return true;
case ShakeAction.fastForward: case ShakeAction.fastForward:
_logger.fine('Fast forwarding'); _logger.fine('Fast forwarding');
if (!player.playing) { if (!player.player.playerState.playing) {
_logger.warning('Player is not playing'); _logger.warning('Player is not playing');
return false; return false;
} }
player.seek(player.position + const Duration(seconds: 30)); player.seek(player.player.position + const Duration(seconds: 30));
return true; return true;
case ShakeAction.rewind: case ShakeAction.rewind:
_logger.fine('Rewinding'); _logger.fine('Rewinding');
if (!player.playing) { if (!player.player.playerState.playing) {
_logger.warning('Player is not playing'); _logger.warning('Player is not playing');
return false; return false;
} }
player.seek(player.position - const Duration(seconds: 30)); player.seek(player.player.position - const Duration(seconds: 30));
return true; return true;
case ShakeAction.playPause: case ShakeAction.playPause:
_logger.fine('Toggling play/pause'); _logger.fine('Toggling play/pause');

View file

@ -6,7 +6,7 @@ part of 'shake_detector.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$shakeDetectorHash() => r'd30daa94f3541bf4d7fa81d5f38dbb7c55c946f7'; String _$shakeDetectorHash() => r'd5f34001dbf6ffb2a114c877f05809c195a58e63';
/// See also [ShakeDetector]. /// See also [ShakeDetector].
@ProviderFor(ShakeDetector) @ProviderFor(ShakeDetector)

View file

@ -1,112 +1,48 @@
import 'dart:async'; import 'dart:async';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/features/player/core/audiobook_player_session.dart';
import 'package:vaani/features/player/core/audiobook_player.dart';
import 'package:vaani/shared/extensions/chapter.dart'; import 'package:vaani/shared/extensions/chapter.dart';
import 'package:vaani/shared/utils/throttler.dart'; import 'package:vaani/shared/utils/throttler.dart';
class SkipStartEnd { class SkipStartEnd {
final Duration start; final Duration start;
final Duration end; final Duration end;
final AudiobookPlayer player; final AbsAudioHandler player;
// id
int? chapterId;
// int _index;
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
final throttler = Throttler(delay: Duration(seconds: 3)); final throttlerStart = Throttler(delay: Duration(seconds: 3));
// final StreamController<PlaybackEvent> _playbackController = final throttlerEnd = Throttler(delay: Duration(seconds: 3));
// StreamController<PlaybackEvent>.broadcast();
SkipStartEnd({ SkipStartEnd({
required this.start, required this.start,
required this.end, required this.end,
required this.player, required this.player,
this.chapterId,
}) { }) {
// if (start > Duration()) { if (start > Duration.zero) {
// _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) {
_subscriptions.add( _subscriptions.add(
player.positionStream.listen((position) { player.chapterStream.listen((chapter) {
final chapter = player.currentChapter; if (chapter != null &&
if (chapter == null) { player.positionInChapter < Duration(seconds: 1)) {
return; Future.microtask(
} () => throttlerStart
if (chapter.id == chapterId) { .call(() => player.seekInBook(chapter.start + start)),
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;
} }
}), }),
); );
} }
} if (end > Duration.zero) {
_subscriptions.add(
void skipStart(BookChapter chapter) { player.positionStreamInChapter.listen((positionChapter) {
print('跳过片头'); if (end >
final globalPosition = player.positionInBook; (player.currentChapter?.duration ?? Duration.zero) -
if (globalPosition - chapter.start < Duration(seconds: 1)) { positionChapter) {
player.seekInBook(chapter.start + start); Future.microtask(
} () => throttlerEnd.call(() => player.skipToNext()),
} );
}
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();
} }
} }
@ -115,7 +51,8 @@ class SkipStartEnd {
for (var sub in _subscriptions) { for (var sub in _subscriptions) {
sub.cancel(); sub.cancel();
} }
throttler.dispose(); throttlerStart.dispose();
throttlerEnd.dispose();
// _playbackController.close(); // _playbackController.close();
} }
} }

View file

@ -1,6 +1,6 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.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; import 'package:vaani/features/skip_start_end/skip_start_end.dart' as core;
part 'skip_start_end_provider.g.dart'; part 'skip_start_end_provider.g.dart';
@ -9,23 +9,51 @@ part 'skip_start_end_provider.g.dart';
class SkipStartEnd extends _$SkipStartEnd { class SkipStartEnd extends _$SkipStartEnd {
@override @override
core.SkipStartEnd? build() { core.SkipStartEnd? build() {
final player = ref.watch(simpleAudiobookPlayerProvider); final session = ref.watch(sessionProvider);
final book = ref.watch(audiobookPlayerProvider.select((v) => v.book)); final bookId = session?.libraryItemId;
final bookId = book?.libraryItemId ?? '_'; if (session == null || bookId == null) {
if (bookId == '_') {
return null; return null;
} }
final player = ref.read(playerProvider);
final bookSettings = ref.watch(bookSettingsProvider(bookId)); final bookSettings = ref.watch(bookSettingsProvider(bookId));
final start = bookSettings.playerSettings.skipChapterStart; final start = bookSettings.playerSettings.skipChapterStart;
final end = bookSettings.playerSettings.skipChapterEnd; final end = bookSettings.playerSettings.skipChapterEnd;
if (start < Duration.zero && end < Duration.zero) {
return null;
}
final skipStartEnd = core.SkipStartEnd( final skipStartEnd = core.SkipStartEnd(
start: start, start: start,
end: end, end: end,
player: player, player: player,
chapterId: player.currentChapter?.id,
); );
ref.onDispose(skipStartEnd.dispose); ref.onDispose(skipStartEnd.dispose);
return skipStartEnd; 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;
// }
// }

View file

@ -6,7 +6,7 @@ part of 'skip_start_end_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$skipStartEndHash() => r'857b448eac9bb9ab85cea9217775712e660bc990'; String _$skipStartEndHash() => r'6df119db598c6e8673dcea090ad97f5affab4016';
/// See also [SkipStartEnd]. /// See also [SkipStartEnd].
@ProviderFor(SkipStartEnd) @ProviderFor(SkipStartEnd)

157
lib/framework.dart Normal file
View file

@ -0,0 +1,157 @@
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/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<Framework> createState() => _FrameworkState();
}
class _FrameworkState extends ConsumerState<Framework>
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.
try {
final audioService = ref.watch(audioHandlerInitProvider);
ref.watch(playbackReporterProvider);
// ref.watch(simpleAudiobookPlayerProvider);
ref.watch(sleepTimerProvider);
// ref.watch(playbackReporterProvider);
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();
}
}
@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();
}
}
}

View file

@ -38,449 +38,483 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"account": MessageLookupByLibrary.simpleMessage("Account"), "account": MessageLookupByLibrary.simpleMessage("Account"),
"accountAddNewServer": MessageLookupByLibrary.simpleMessage( "accountAddNewServer": MessageLookupByLibrary.simpleMessage(
"Add New Server", "Add New Server",
), ),
"accountAddUser": MessageLookupByLibrary.simpleMessage("Add User"), "accountAddUser": MessageLookupByLibrary.simpleMessage("Add User"),
"accountAddUserDialog": m0, "accountAddUserDialog": m0,
"accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage( "accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage(
"User added successfully! Switch?", "User added successfully! Switch?",
), ),
"accountAddUserTooltip": MessageLookupByLibrary.simpleMessage( "accountAddUserTooltip": MessageLookupByLibrary.simpleMessage(
"Add new server", "Add new server",
), ),
"accountAnonymous": MessageLookupByLibrary.simpleMessage("Anonymous"), "accountAnonymous": MessageLookupByLibrary.simpleMessage("Anonymous"),
"accountDeleteServer": MessageLookupByLibrary.simpleMessage( "accountDeleteServer": MessageLookupByLibrary.simpleMessage(
"Delete Server", "Delete Server",
), ),
"accountInvalidURL": MessageLookupByLibrary.simpleMessage("Invalid URL"), "accountInvalidURL":
"accountManage": MessageLookupByLibrary.simpleMessage("Manage Accounts"), MessageLookupByLibrary.simpleMessage("Invalid URL"),
"accountRegisteredServers": MessageLookupByLibrary.simpleMessage( "accountManage":
"Registered Servers", MessageLookupByLibrary.simpleMessage("Manage Accounts"),
), "accountRegisteredServers": MessageLookupByLibrary.simpleMessage(
"accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( "Registered Servers",
"Remove Server and Users", ),
), "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage(
"accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage( "Remove Server and Users",
"This will remove the server ", ),
), "accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage(
"accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( "This will remove the server ",
" and all its users\' login info from this app.", ),
), "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage(
"accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage( " and all its users\' login info from this app.",
"Remove User Login", ),
), "accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage(
"accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( "Remove User Login",
"This will remove login details of the user ", ),
), "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage(
"accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage( "This will remove login details of the user ",
" from this app.", ),
), "accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage(
"accountServerURI": MessageLookupByLibrary.simpleMessage("Server URI"), " from this app.",
"accountSwitch": MessageLookupByLibrary.simpleMessage("Switch Account"), ),
"accountUsersCount": m1, "accountServerURI": MessageLookupByLibrary.simpleMessage("Server URI"),
"appSettings": MessageLookupByLibrary.simpleMessage("App Settings"), "accountSwitch": MessageLookupByLibrary.simpleMessage("Switch Account"),
"appearance": MessageLookupByLibrary.simpleMessage("Appearance"), "accountUsersCount": m1,
"autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage( "appSettings": MessageLookupByLibrary.simpleMessage("App Settings"),
"Auto Sleep Timer Settings", "appearance": MessageLookupByLibrary.simpleMessage("Appearance"),
), "autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage(
"autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage( "Auto Sleep Timer Settings",
"Auto Turn On Sleep Timer", ),
), "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimer": MessageLookupByLibrary.simpleMessage( "Auto Turn On Sleep Timer",
"Auto Turn On Timer", ),
), "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage( "Auto Turn On Timer",
"Always Auto Turn On Timer", ),
), "autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimerAlwaysDescription": MessageLookupByLibrary.simpleMessage( "Always Auto Turn On Timer",
"Always turn on the sleep timer, no matter what", ),
), "autoTurnOnTimerAlwaysDescription":
"autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"Automatically turn on the sleep timer based on the time of day", "Always turn on the sleep timer, no matter what",
), ),
"autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage("From"), "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage( "Automatically turn on the sleep timer based on the time of day",
"Turn on the sleep timer at the specified time", ),
), "autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage("From"),
"autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("Until"), "autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage( "Turn on the sleep timer at the specified time",
"Turn off the sleep timer at the specified time", ),
), "autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("Until"),
"automaticallyDescription": MessageLookupByLibrary.simpleMessage( "autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage(
"Automatically turn on the sleep timer based on the time of day", "Turn off the sleep timer at the specified time",
), ),
"backup": MessageLookupByLibrary.simpleMessage("Backup"), "automaticallyDescription": MessageLookupByLibrary.simpleMessage(
"backupAndRestore": MessageLookupByLibrary.simpleMessage( "Automatically turn on the sleep timer based on the time of day",
"Backup and Restore", ),
), "backup": MessageLookupByLibrary.simpleMessage("Backup"),
"bookAbout": MessageLookupByLibrary.simpleMessage("About the Book"), "backupAndRestore": MessageLookupByLibrary.simpleMessage(
"bookAboutDefault": MessageLookupByLibrary.simpleMessage( "Backup and Restore",
"Sorry, no description found", ),
), "bookAbout": MessageLookupByLibrary.simpleMessage("About the Book"),
"bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"), "bookAboutDefault": MessageLookupByLibrary.simpleMessage(
"bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"), "Sorry, no description found",
"bookGenres": MessageLookupByLibrary.simpleMessage("Genres"), ),
"bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"), "bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"),
"bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), "bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"),
"bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"), "bookGenres": MessageLookupByLibrary.simpleMessage("Genres"),
"bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( "bookMetadataAbridged":
"Unabridged", MessageLookupByLibrary.simpleMessage("Abridged"),
), "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"),
"bookSeries": MessageLookupByLibrary.simpleMessage("Series"), "bookMetadataPublished":
"bookShelveEmpty": MessageLookupByLibrary.simpleMessage("Try again"), MessageLookupByLibrary.simpleMessage("Published"),
"bookShelveEmptyText": MessageLookupByLibrary.simpleMessage( "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage(
"No shelves to display", "Unabridged",
), ),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "bookSeries": MessageLookupByLibrary.simpleMessage("Series"),
"copyToClipboard": MessageLookupByLibrary.simpleMessage( "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("Try again"),
"Copy to Clipboard", "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage(
), "No shelves to display",
"copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( ),
"Copy the app settings to the clipboard", "cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
), "chapterNotFound": MessageLookupByLibrary.simpleMessage("Chapters"),
"copyToClipboardToast": MessageLookupByLibrary.simpleMessage( "chapterSelect": MessageLookupByLibrary.simpleMessage("Select Chapter"),
"Settings copied to clipboard", "chapterSkip": MessageLookupByLibrary.simpleMessage(
), "Skip chapter opening and ending",
"delete": MessageLookupByLibrary.simpleMessage("Delete"), ),
"deleteDialog": m2, "chapterSkipEnd": MessageLookupByLibrary.simpleMessage(
"deleted": m3, "Skip chapter opening for ",
"explore": MessageLookupByLibrary.simpleMessage("explore"), ),
"exploreHint": MessageLookupByLibrary.simpleMessage( "chapterSkipOpen": MessageLookupByLibrary.simpleMessage(
"Seek and you shall discover...", "Skip chapter opening for ",
), ),
"exploreTooltip": MessageLookupByLibrary.simpleMessage( "chapters": MessageLookupByLibrary.simpleMessage("Chapters"),
"Search and Explore", "copyToClipboard": MessageLookupByLibrary.simpleMessage(
), "Copy to Clipboard",
"general": MessageLookupByLibrary.simpleMessage("General"), ),
"help": MessageLookupByLibrary.simpleMessage("Help"), "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage(
"home": MessageLookupByLibrary.simpleMessage("Home"), "Copy the app settings to the clipboard",
"homeBookContinueListening": MessageLookupByLibrary.simpleMessage( ),
"Continue Listening", "copyToClipboardToast": MessageLookupByLibrary.simpleMessage(
), "Settings copied to clipboard",
"homeBookContinueListeningDescription": ),
MessageLookupByLibrary.simpleMessage( "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", "Show play button for books in currently listening shelf",
), ),
"homeBookContinueSeries": MessageLookupByLibrary.simpleMessage( "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage(
"Continue Series", "Continue Series",
), ),
"homeBookContinueSeriesDescription": MessageLookupByLibrary.simpleMessage( "homeBookContinueSeriesDescription":
"Show play button for books in continue series shelf", MessageLookupByLibrary.simpleMessage(
), "Show play button for books in continue series shelf",
"homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"), ),
"homeBookListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), "homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"),
"homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( "homeBookListenAgain":
"Show play button for all books in listen again shelf", MessageLookupByLibrary.simpleMessage("Listen Again"),
), "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage(
"homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage( "Show play button for all books in listen again shelf",
"Newest Authors", ),
), "homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage(
"homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage( "Newest Authors",
"Recently Added", ),
), "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage(
"homeBookRecommended": MessageLookupByLibrary.simpleMessage("Recommended"), "Recently Added",
"homeContinueListening": MessageLookupByLibrary.simpleMessage( ),
"Continue Listening", "homeBookRecommended":
), MessageLookupByLibrary.simpleMessage("Recommended"),
"homeListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), "homeContinueListening": MessageLookupByLibrary.simpleMessage(
"homePageSettings": MessageLookupByLibrary.simpleMessage( "Continue Listening",
"Home Page Settings", ),
), "homeListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"),
"homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( "homePageSettings": MessageLookupByLibrary.simpleMessage(
"Customize the home page", "Home Page Settings",
), ),
"homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage( "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage(
"Other shelves", "Customize the home page",
), ),
"homePageSettingsOtherShelvesDescription": "homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage( "Other shelves",
),
"homePageSettingsOtherShelvesDescription":
MessageLookupByLibrary.simpleMessage(
"Show play button for all books in all remaining shelves", "Show play button for all books in all remaining shelves",
), ),
"homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage( "homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage(
"Quick Play", "Quick Play",
), ),
"homeStartListening": MessageLookupByLibrary.simpleMessage( "homeStartListening": MessageLookupByLibrary.simpleMessage(
"Start Listening", "Start Listening",
), ),
"language": MessageLookupByLibrary.simpleMessage("Language"), "language": MessageLookupByLibrary.simpleMessage("Language"),
"languageDescription": MessageLookupByLibrary.simpleMessage( "languageDescription": MessageLookupByLibrary.simpleMessage(
"Language switch", "Language switch",
), ),
"library": MessageLookupByLibrary.simpleMessage("Library"), "library": MessageLookupByLibrary.simpleMessage("Library"),
"libraryChange": MessageLookupByLibrary.simpleMessage("Change Library"), "libraryChange": MessageLookupByLibrary.simpleMessage("Change Library"),
"libraryEmpty": MessageLookupByLibrary.simpleMessage( "libraryEmpty": MessageLookupByLibrary.simpleMessage(
"No libraries available.", "No libraries available.",
), ),
"libraryLoadError": m4, "libraryLoadError": m4,
"librarySelect": MessageLookupByLibrary.simpleMessage("Select Library"), "librarySelect": MessageLookupByLibrary.simpleMessage("Select Library"),
"librarySwitchTooltip": MessageLookupByLibrary.simpleMessage( "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage(
"Switch Library", "Switch Library",
), ),
"libraryTooltip": MessageLookupByLibrary.simpleMessage( "libraryTooltip": MessageLookupByLibrary.simpleMessage(
"Browse your library", "Browse your library",
), ),
"loading": MessageLookupByLibrary.simpleMessage("Loading..."), "loading": MessageLookupByLibrary.simpleMessage("Loading..."),
"loginLocal": MessageLookupByLibrary.simpleMessage("Local"), "loginLocal": MessageLookupByLibrary.simpleMessage("Local"),
"loginLogin": MessageLookupByLibrary.simpleMessage("Login"), "loginLogin": MessageLookupByLibrary.simpleMessage("Login"),
"loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"),
"loginPassword": MessageLookupByLibrary.simpleMessage("Password"), "loginPassword": MessageLookupByLibrary.simpleMessage("Password"),
"loginServerClick": MessageLookupByLibrary.simpleMessage("Click here"), "loginServerClick": MessageLookupByLibrary.simpleMessage("Click here"),
"loginServerConnected": MessageLookupByLibrary.simpleMessage( "loginServerConnected": MessageLookupByLibrary.simpleMessage(
"Server connected, please login", "Server connected, please login",
), ),
"loginServerNo": MessageLookupByLibrary.simpleMessage( "loginServerNo": MessageLookupByLibrary.simpleMessage(
"Do not have a server? ", "Do not have a server? ",
), ),
"loginServerNoConnected": MessageLookupByLibrary.simpleMessage( "loginServerNoConnected": MessageLookupByLibrary.simpleMessage(
"Please enter the URL of your AudiobookShelf Server", "Please enter the URL of your AudiobookShelf Server",
), ),
"loginServerNot": m5, "loginServerNot": m5,
"loginServerTo": MessageLookupByLibrary.simpleMessage( "loginServerTo": MessageLookupByLibrary.simpleMessage(
" to know how to setup a server.", " to know how to setup a server.",
), ),
"loginTitle": m6, "loginTitle": m6,
"loginToken": MessageLookupByLibrary.simpleMessage("Token"), "loginToken": MessageLookupByLibrary.simpleMessage("Token"),
"loginUsername": MessageLookupByLibrary.simpleMessage("Username"), "loginUsername": MessageLookupByLibrary.simpleMessage("Username"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"), "logs": MessageLookupByLibrary.simpleMessage("Logs"),
"nmpSettingsBackward": MessageLookupByLibrary.simpleMessage( "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage(
"Backward Interval", "Backward Interval",
), ),
"nmpSettingsForward": MessageLookupByLibrary.simpleMessage( "nmpSettingsForward": MessageLookupByLibrary.simpleMessage(
"Forward Interval", "Forward Interval",
), ),
"nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage( "nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage(
"Media Controls", "Media Controls",
), ),
"nmpSettingsMediaControlsDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsMediaControlsDescription":
"Select the media controls to display", MessageLookupByLibrary.simpleMessage(
), "Select the media controls to display",
"nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( ),
"Select a field below to insert it", "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage(
), "Select a field below to insert it",
"nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage( ),
"Show Chapter Progress", "nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage(
), "Show Chapter Progress",
"nmpSettingsShowChapterProgressDescription": ),
MessageLookupByLibrary.simpleMessage( "nmpSettingsShowChapterProgressDescription":
MessageLookupByLibrary.simpleMessage(
"Instead of the overall progress of the book", "Instead of the overall progress of the book",
), ),
"nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage( "nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage(
"Secondary Title", "Secondary Title",
), ),
"nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage(
"The subtitle of the notification\n", "The subtitle of the notification\n",
), ),
"nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("Primary Title"), "nmpSettingsTitle":
"nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("Primary Title"),
"The title of the notification\n", "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage(
), "The title of the notification\n",
"no": MessageLookupByLibrary.simpleMessage("No"), ),
"notImplemented": MessageLookupByLibrary.simpleMessage("Not implemented"), "no": MessageLookupByLibrary.simpleMessage("No"),
"notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( "notImplemented":
"Notification Media Player", MessageLookupByLibrary.simpleMessage("Not implemented"),
), "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage(
"notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( "Notification Media Player",
"Customize the media player in notifications", ),
), "notificationMediaPlayerDescription":
"ok": MessageLookupByLibrary.simpleMessage("OK"), MessageLookupByLibrary.simpleMessage(
"pause": MessageLookupByLibrary.simpleMessage("Pause"), "Customize the media player in notifications",
"play": MessageLookupByLibrary.simpleMessage("Play"), ),
"playerSettings": MessageLookupByLibrary.simpleMessage("Player Settings"), "ok": MessageLookupByLibrary.simpleMessage("OK"),
"playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( "pause": MessageLookupByLibrary.simpleMessage("Pause"),
"Mark Complete When Time Left", "play": MessageLookupByLibrary.simpleMessage("Play"),
), "playerSettings":
"playerSettingsCompleteTimeDescriptionHead": MessageLookupByLibrary.simpleMessage("Player Settings"),
MessageLookupByLibrary.simpleMessage("Mark complete when less than "), "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage(
"playerSettingsCompleteTimeDescriptionTail": "Mark Complete When Time Left",
MessageLookupByLibrary.simpleMessage(" left in the book"), ),
"playerSettingsDescription": MessageLookupByLibrary.simpleMessage( "playerSettingsCompleteTimeDescriptionHead":
"Customize the player settings", MessageLookupByLibrary.simpleMessage(
), "Mark complete when less than "),
"playerSettingsDisplay": MessageLookupByLibrary.simpleMessage( "playerSettingsCompleteTimeDescriptionTail":
"Display Settings", MessageLookupByLibrary.simpleMessage(" left in the book"),
), "playerSettingsDescription": MessageLookupByLibrary.simpleMessage(
"playerSettingsDisplayChapterProgress": "Customize the player settings",
MessageLookupByLibrary.simpleMessage("Show Chapter Progress"), ),
"playerSettingsDisplayChapterProgressDescription": "playerSettingsDisplay": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage( "Display Settings",
),
"playerSettingsDisplayChapterProgress":
MessageLookupByLibrary.simpleMessage("Show Chapter Progress"),
"playerSettingsDisplayChapterProgressDescription":
MessageLookupByLibrary.simpleMessage(
"Show the progress of the current chapter in the player", "Show the progress of the current chapter in the player",
), ),
"playerSettingsDisplayTotalProgress": MessageLookupByLibrary.simpleMessage( "playerSettingsDisplayTotalProgress":
"Show Total Progress", MessageLookupByLibrary.simpleMessage(
), "Show Total Progress",
"playerSettingsDisplayTotalProgressDescription": ),
MessageLookupByLibrary.simpleMessage( "playerSettingsDisplayTotalProgressDescription":
MessageLookupByLibrary.simpleMessage(
"Show the total progress of the book in the player", "Show the total progress of the book in the player",
), ),
"playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage( "playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage(
"Playback Report Interval", "Playback Report Interval",
), ),
"playerSettingsPlaybackIntervalDescriptionHead": "playerSettingsPlaybackIntervalDescriptionHead":
MessageLookupByLibrary.simpleMessage("Report progress every "), MessageLookupByLibrary.simpleMessage("Report progress every "),
"playerSettingsPlaybackIntervalDescriptionTail": "playerSettingsPlaybackIntervalDescriptionTail":
MessageLookupByLibrary.simpleMessage(" to the server"), MessageLookupByLibrary.simpleMessage(" to the server"),
"playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage( "playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage(
"Playback Reporting", "Playback Reporting",
), ),
"playerSettingsPlaybackReportingIgnore": "playerSettingsPlaybackReportingIgnore":
MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"Ignore Playback Position Less Than", "Ignore Playback Position Less Than",
), ),
"playerSettingsPlaybackReportingMinimum": "playerSettingsPlaybackReportingMinimum":
MessageLookupByLibrary.simpleMessage("Minimum Position to Report"), MessageLookupByLibrary.simpleMessage("Minimum Position to Report"),
"playerSettingsPlaybackReportingMinimumDescriptionHead": "playerSettingsPlaybackReportingMinimumDescriptionHead":
MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"Do not report playback for the first ", "Do not report playback for the first ",
), ),
"playerSettingsPlaybackReportingMinimumDescriptionTail": "playerSettingsPlaybackReportingMinimumDescriptionTail":
MessageLookupByLibrary.simpleMessage("of the book"), MessageLookupByLibrary.simpleMessage("of the book"),
"playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage( "playerSettingsRememberForEveryBook":
"Remember Player Settings for Every Book", MessageLookupByLibrary.simpleMessage(
), "Remember Player Settings for Every Book",
"playerSettingsRememberForEveryBookDescription": ),
MessageLookupByLibrary.simpleMessage( "playerSettingsRememberForEveryBookDescription":
MessageLookupByLibrary.simpleMessage(
"Settings like speed, loudness, etc. will be remembered for every book", "Settings like speed, loudness, etc. will be remembered for every book",
), ),
"playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("Speed"), "playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("Speed"),
"playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage(
"Default Speed", "Default Speed",
), ),
"playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage(
"Speed Options", "Speed Options",
), ),
"playerSettingsSpeedOptionsSelect": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptionsSelect":
"Select Speed Options", MessageLookupByLibrary.simpleMessage(
), "Select Speed Options",
"playerSettingsSpeedOptionsSelectAdd": MessageLookupByLibrary.simpleMessage( ),
"Add Speed Option", "playerSettingsSpeedOptionsSelectAdd":
), MessageLookupByLibrary.simpleMessage(
"playerSettingsSpeedOptionsSelectAddHelper": "Add Speed Option",
MessageLookupByLibrary.simpleMessage("Enter a new speed option to add"), ),
"playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptionsSelectAddHelper":
"Select Speed", MessageLookupByLibrary.simpleMessage(
), "Enter a new speed option to add"),
"playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage(
"Enter the speed you want to set when playing for the first time", "Select Speed",
), ),
"playlistsMine": MessageLookupByLibrary.simpleMessage("My Playlists"), "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage(
"readLess": MessageLookupByLibrary.simpleMessage("Read Less"), "Enter the speed you want to set when playing for the first time",
"readMore": MessageLookupByLibrary.simpleMessage("Read More"), ),
"refresh": MessageLookupByLibrary.simpleMessage("Refresh"), "playlistsMine": MessageLookupByLibrary.simpleMessage("My Playlists"),
"reset": MessageLookupByLibrary.simpleMessage("Reset"), "readLess": MessageLookupByLibrary.simpleMessage("Read Less"),
"resetAppSettings": MessageLookupByLibrary.simpleMessage( "readMore": MessageLookupByLibrary.simpleMessage("Read More"),
"Reset App Settings", "refresh": MessageLookupByLibrary.simpleMessage("Refresh"),
), "reset": MessageLookupByLibrary.simpleMessage("Reset"),
"resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( "resetAppSettings": MessageLookupByLibrary.simpleMessage(
"Reset the app settings to the default values", "Reset App Settings",
), ),
"resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage(
"Are you sure you want to reset the app settings?", "Reset the app settings to the default values",
), ),
"restore": MessageLookupByLibrary.simpleMessage("Restore"), "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage(
"restoreBackup": MessageLookupByLibrary.simpleMessage("Restore Backup"), "Are you sure you want to reset the app settings?",
"restoreBackupHint": MessageLookupByLibrary.simpleMessage( ),
"Paste the backup here", "restore": MessageLookupByLibrary.simpleMessage("Restore"),
), "restoreBackup": MessageLookupByLibrary.simpleMessage("Restore Backup"),
"restoreBackupInvalid": MessageLookupByLibrary.simpleMessage( "restoreBackupHint": MessageLookupByLibrary.simpleMessage(
"Invalid backup", "Paste the backup here",
), ),
"restoreBackupSuccess": MessageLookupByLibrary.simpleMessage( "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage(
"Settings restored", "Invalid backup",
), ),
"restoreBackupValidator": MessageLookupByLibrary.simpleMessage( "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage(
"Please paste the backup here", "Settings restored",
), ),
"restoreDescription": MessageLookupByLibrary.simpleMessage( "restoreBackupValidator": MessageLookupByLibrary.simpleMessage(
"Restore the app settings from the backup", "Please paste the backup here",
), ),
"resume": MessageLookupByLibrary.simpleMessage("Resume"), "restoreDescription": MessageLookupByLibrary.simpleMessage(
"retry": MessageLookupByLibrary.simpleMessage("Retry"), "Restore the app settings from the backup",
"settings": MessageLookupByLibrary.simpleMessage("Settings"), ),
"shakeAction": MessageLookupByLibrary.simpleMessage("Shake Action"), "resume": MessageLookupByLibrary.simpleMessage("Resume"),
"shakeActionDescription": MessageLookupByLibrary.simpleMessage( "retry": MessageLookupByLibrary.simpleMessage("Retry"),
"The action to perform when a shake is detected", "settings": MessageLookupByLibrary.simpleMessage("Settings"),
), "shakeAction": MessageLookupByLibrary.simpleMessage("Shake Action"),
"shakeActivationThreshold": MessageLookupByLibrary.simpleMessage( "shakeActionDescription": MessageLookupByLibrary.simpleMessage(
"Shake Activation Threshold", "The action to perform when a shake is detected",
), ),
"shakeActivationThresholdDescription": MessageLookupByLibrary.simpleMessage( "shakeActivationThreshold": MessageLookupByLibrary.simpleMessage(
"The higher the threshold, the harder you need to shake", "Shake Activation Threshold",
), ),
"shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), "shakeActivationThresholdDescription":
"shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"Customize the shake detector settings", "The higher the threshold, the harder you need to shake",
), ),
"shakeDetectorEnable": MessageLookupByLibrary.simpleMessage( "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"),
"Enable Shake Detection", "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage(
), "Customize the shake detector settings",
"shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( ),
"Enable shake detection to do various actions", "shakeDetectorEnable": MessageLookupByLibrary.simpleMessage(
), "Enable Shake Detection",
"shakeDetectorSettings": MessageLookupByLibrary.simpleMessage( ),
"Shake Detector Settings", "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage(
), "Enable shake detection to do various actions",
"shakeFeedback": MessageLookupByLibrary.simpleMessage("Shake Feedback"), ),
"shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( "shakeDetectorSettings": MessageLookupByLibrary.simpleMessage(
"The feedback to give when a shake is detected", "Shake Detector Settings",
), ),
"shakeSelectAction": MessageLookupByLibrary.simpleMessage( "shakeFeedback": MessageLookupByLibrary.simpleMessage("Shake Feedback"),
"Select Shake Action", "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage(
), "The feedback to give when a shake is detected",
"shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage( ),
"Select Shake Activation Threshold", "shakeSelectAction": MessageLookupByLibrary.simpleMessage(
), "Select Shake Action",
"shakeSelectActivationThresholdHelper": ),
MessageLookupByLibrary.simpleMessage( "shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage(
"Select Shake Activation Threshold",
),
"shakeSelectActivationThresholdHelper":
MessageLookupByLibrary.simpleMessage(
"Enter a number to set the threshold in m/s²", "Enter a number to set the threshold in m/s²",
), ),
"shakeSelectFeedback": MessageLookupByLibrary.simpleMessage( "shakeSelectFeedback": MessageLookupByLibrary.simpleMessage(
"Select Shake Feedback", "Select Shake Feedback",
), ),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme Mode"), "themeMode": MessageLookupByLibrary.simpleMessage("Theme Mode"),
"themeModeDark": MessageLookupByLibrary.simpleMessage("Dark"), "themeModeDark": MessageLookupByLibrary.simpleMessage("Dark"),
"themeModeHighContrast": MessageLookupByLibrary.simpleMessage( "themeModeHighContrast": MessageLookupByLibrary.simpleMessage(
"High Contrast Mode", "High Contrast Mode",
), ),
"themeModeHighContrastDescription": MessageLookupByLibrary.simpleMessage( "themeModeHighContrastDescription":
"Increase the contrast between the background and the text", MessageLookupByLibrary.simpleMessage(
), "Increase the contrast between the background and the text",
"themeModeLight": MessageLookupByLibrary.simpleMessage("Light"), ),
"themeModeSystem": MessageLookupByLibrary.simpleMessage("System"), "themeModeLight": MessageLookupByLibrary.simpleMessage("Light"),
"themeSettings": MessageLookupByLibrary.simpleMessage("Theme Settings"), "themeModeSystem": MessageLookupByLibrary.simpleMessage("System"),
"themeSettingsColors": MessageLookupByLibrary.simpleMessage( "themeSettings": MessageLookupByLibrary.simpleMessage("Theme Settings"),
"Material Theme from System", "themeSettingsColors": MessageLookupByLibrary.simpleMessage(
), "Material Theme from System",
"themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage( ),
"Use Material You", "themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage(
), "Use Material You",
"themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( ),
"Adaptive Theme on Item Page", "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", "themeSettingsColorsBookDescription":
), MessageLookupByLibrary.simpleMessage(
"themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( "Get fancy with the colors on the item page at the cost of some performance",
"Adapt theme from currently playing item", ),
), "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage(
"themeSettingsColorsCurrentDescription": "Adapt theme from currently playing item",
MessageLookupByLibrary.simpleMessage( ),
"themeSettingsColorsCurrentDescription":
MessageLookupByLibrary.simpleMessage(
"Use the theme colors from the currently playing item for the app", "Use the theme colors from the currently playing item for the app",
), ),
"themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage(
"Use the system theme colors for the app", "Use the system theme colors for the app",
), ),
"themeSettingsDescription": MessageLookupByLibrary.simpleMessage( "themeSettingsDescription": MessageLookupByLibrary.simpleMessage(
"Customize the app theme", "Customize the app theme",
), ),
"timeSecond": m7, "timeSecond": m7,
"unknown": MessageLookupByLibrary.simpleMessage("Unknown"), "unknown": MessageLookupByLibrary.simpleMessage("Unknown"),
"webVersion": MessageLookupByLibrary.simpleMessage("Web Version"), "webVersion": MessageLookupByLibrary.simpleMessage("Web Version"),
"yes": MessageLookupByLibrary.simpleMessage("Yes"), "yes": MessageLookupByLibrary.simpleMessage("Yes"),
"you": MessageLookupByLibrary.simpleMessage("You"), "you": MessageLookupByLibrary.simpleMessage("You"),
"youTooltip": MessageLookupByLibrary.simpleMessage( "youTooltip": MessageLookupByLibrary.simpleMessage(
"Your Profile and Settings", "Your Profile and Settings",
), ),
}; };
} }

View file

@ -38,319 +38,354 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"account": MessageLookupByLibrary.simpleMessage("账户"), "account": MessageLookupByLibrary.simpleMessage("账户"),
"accountAddNewServer": MessageLookupByLibrary.simpleMessage("添加新服务器"), "accountAddNewServer": MessageLookupByLibrary.simpleMessage("添加新服务器"),
"accountAddUser": MessageLookupByLibrary.simpleMessage("添加用户"), "accountAddUser": MessageLookupByLibrary.simpleMessage("添加用户"),
"accountAddUserDialog": m0, "accountAddUserDialog": m0,
"accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage( "accountAddUserSuccessDialog": MessageLookupByLibrary.simpleMessage(
"用户添加成功!切换?", "用户添加成功!切换?",
), ),
"accountAddUserTooltip": MessageLookupByLibrary.simpleMessage("添加新服务器"), "accountAddUserTooltip": MessageLookupByLibrary.simpleMessage("添加新服务器"),
"accountAnonymous": MessageLookupByLibrary.simpleMessage("匿名"), "accountAnonymous": MessageLookupByLibrary.simpleMessage("匿名"),
"accountDeleteServer": MessageLookupByLibrary.simpleMessage("删除服务器"), "accountDeleteServer": MessageLookupByLibrary.simpleMessage("删除服务器"),
"accountInvalidURL": MessageLookupByLibrary.simpleMessage("无效网址"), "accountInvalidURL": MessageLookupByLibrary.simpleMessage("无效网址"),
"accountManage": MessageLookupByLibrary.simpleMessage("帐户管理"), "accountManage": MessageLookupByLibrary.simpleMessage("帐户管理"),
"accountRegisteredServers": MessageLookupByLibrary.simpleMessage("已注册服务器"), "accountRegisteredServers":
"accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("已注册服务器"),
"删除服务器和用户", "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage(
), "删除服务器和用户",
"accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage( ),
"这将删除服务器 ", "accountRemoveServerAndUsersHead": MessageLookupByLibrary.simpleMessage(
), "这将删除服务器 ",
"accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( ),
" 以及该应用程序中所有用户的登录信息。", "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage(
), " 以及该应用程序中所有用户的登录信息。",
"accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage("删除用户登录"), ),
"accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( "accountRemoveUserLogin":
"这将删除用户 ", MessageLookupByLibrary.simpleMessage("删除用户登录"),
), "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage(
"accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage( "这将删除用户 ",
" 的登录详细信息。", ),
), "accountRemoveUserLoginTail": MessageLookupByLibrary.simpleMessage(
"accountServerURI": MessageLookupByLibrary.simpleMessage("服务器地址"), " 的登录详细信息。",
"accountSwitch": MessageLookupByLibrary.simpleMessage("切换账户"), ),
"accountUsersCount": m1, "accountServerURI": MessageLookupByLibrary.simpleMessage("服务器地址"),
"appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), "accountSwitch": MessageLookupByLibrary.simpleMessage("切换账户"),
"appearance": MessageLookupByLibrary.simpleMessage("外观"), "accountUsersCount": m1,
"autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage("自动睡眠定时器设置"), "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"),
"autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), "appearance": MessageLookupByLibrary.simpleMessage("外观"),
"autoTurnOnTimer": MessageLookupByLibrary.simpleMessage("自动开启定时器"), "autoSleepTimerSettings":
"autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage("始终自动开启定时器"), MessageLookupByLibrary.simpleMessage("自动睡眠定时器设置"),
"autoTurnOnTimerAlwaysDescription": MessageLookupByLibrary.simpleMessage( "autoTurnOnSleepTimer":
"总是打开睡眠定时器", MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"),
), "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage("自动开启定时器"),
"autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( "autoTurnOnTimerAlways":
"根据一天中的时间自动打开睡眠定时器", MessageLookupByLibrary.simpleMessage("始终自动开启定时器"),
), "autoTurnOnTimerAlwaysDescription":
"autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage(""), MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage( "总是打开睡眠定时器",
"在指定时间打开睡眠定时器", ),
), "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage(
"autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("直到"), "根据一天中的时间自动打开睡眠定时器",
"autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage( ),
"在指定时间关闭睡眠定时器", "autoTurnOnTimerFrom": MessageLookupByLibrary.simpleMessage(""),
), "autoTurnOnTimerFromDescription": MessageLookupByLibrary.simpleMessage(
"automaticallyDescription": MessageLookupByLibrary.simpleMessage( "在指定时间打开睡眠定时器",
"根据一天中的时间自动打开睡眠定时器", ),
), "autoTurnOnTimerUntil": MessageLookupByLibrary.simpleMessage("直到"),
"backup": MessageLookupByLibrary.simpleMessage("备份"), "autoTurnOnTimerUntilDescription": MessageLookupByLibrary.simpleMessage(
"backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"), "在指定时间关闭睡眠定时器",
"bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"), ),
"bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"), "automaticallyDescription": MessageLookupByLibrary.simpleMessage(
"bookAuthors": MessageLookupByLibrary.simpleMessage("作者"), "根据一天中的时间自动打开睡眠定时器",
"bookDownloads": MessageLookupByLibrary.simpleMessage("下载"), ),
"bookGenres": MessageLookupByLibrary.simpleMessage("风格"), "backup": MessageLookupByLibrary.simpleMessage("备份"),
"bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("删节版"), "backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"bookMetadataLength": MessageLookupByLibrary.simpleMessage("持续时间"), "bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"),
"bookMetadataPublished": MessageLookupByLibrary.simpleMessage("发布年份"), "bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"),
"bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage("未删节版"), "bookAuthors": MessageLookupByLibrary.simpleMessage("作者"),
"bookSeries": MessageLookupByLibrary.simpleMessage("系列"), "bookDownloads": MessageLookupByLibrary.simpleMessage("下载"),
"bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"), "bookGenres": MessageLookupByLibrary.simpleMessage("风格"),
"bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"), "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("删节版"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"), "bookMetadataLength": MessageLookupByLibrary.simpleMessage("持续时间"),
"copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("发布年份"),
"copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage("未删节版"),
"将应用程序设置复制到剪贴板", "bookSeries": MessageLookupByLibrary.simpleMessage("系列"),
), "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"),
"copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "cancel": MessageLookupByLibrary.simpleMessage("取消"),
"deleteDialog": m2, "chapterNotFound": MessageLookupByLibrary.simpleMessage("未找到章节"),
"deleted": m3, "chapterSelect": MessageLookupByLibrary.simpleMessage("选择章节"),
"explore": MessageLookupByLibrary.simpleMessage("探索"), "chapterSkip": MessageLookupByLibrary.simpleMessage("跳过章节片头片尾"),
"exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."), "chapterSkipEnd": MessageLookupByLibrary.simpleMessage("跳过章节片尾 "),
"exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"), "chapterSkipOpen": MessageLookupByLibrary.simpleMessage("跳过章节片头 "),
"general": MessageLookupByLibrary.simpleMessage("通用"), "chapters": MessageLookupByLibrary.simpleMessage("章节列表"),
"help": MessageLookupByLibrary.simpleMessage("Help"), "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"),
"home": MessageLookupByLibrary.simpleMessage("首页"), "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage(
"homeBookContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), "将应用程序设置复制到剪贴板",
"homeBookContinueListeningDescription": ),
MessageLookupByLibrary.simpleMessage("继续收听书架上显示播放按钮"), "copyToClipboardToast":
"homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"), MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"),
"homeBookContinueSeriesDescription": MessageLookupByLibrary.simpleMessage( "delete": MessageLookupByLibrary.simpleMessage("删除"),
"继续系列书架上显示播放按钮", "deleteDialog": m2,
), "deleted": m3,
"homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"), "explore": MessageLookupByLibrary.simpleMessage("探索"),
"homeBookListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), "exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."),
"homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( "exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"),
"再听一遍书架上显示播放按钮", "general": MessageLookupByLibrary.simpleMessage("通用"),
), "help": MessageLookupByLibrary.simpleMessage("Help"),
"homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage("最新作者"), "home": MessageLookupByLibrary.simpleMessage("首页"),
"homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage("最近添加"), "homeBookContinueListening":
"homeBookRecommended": MessageLookupByLibrary.simpleMessage("推荐"), MessageLookupByLibrary.simpleMessage("继续收听"),
"homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), "homeBookContinueListeningDescription":
"homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), MessageLookupByLibrary.simpleMessage("继续收听书架上显示播放按钮"),
"homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"), "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"),
"homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( "homeBookContinueSeriesDescription":
"自定义主页", MessageLookupByLibrary.simpleMessage(
), "继续系列书架上显示播放按钮",
"homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage( ),
"其他书架", "homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"),
), "homeBookListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"),
"homePageSettingsOtherShelvesDescription": "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("显示所有剩余书架上所有书籍的播放按钮"), "再听一遍书架上显示播放按钮",
"homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage("继续播放"), ),
"homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"), "homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage("最新作者"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage("最近添加"),
"languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), "homeBookRecommended": MessageLookupByLibrary.simpleMessage("推荐"),
"library": MessageLookupByLibrary.simpleMessage("媒体库"), "homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"),
"libraryChange": MessageLookupByLibrary.simpleMessage("更改媒体库"), "homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"),
"libraryEmpty": MessageLookupByLibrary.simpleMessage("没有可用的库。"), "homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"),
"libraryLoadError": m4, "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage(
"librarySelect": MessageLookupByLibrary.simpleMessage("选择媒体库"), "自定义主页",
"librarySwitchTooltip": MessageLookupByLibrary.simpleMessage("切换媒体库"), ),
"libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"), "homePageSettingsOtherShelves": MessageLookupByLibrary.simpleMessage(
"loading": MessageLookupByLibrary.simpleMessage("加载中..."), "其他书架",
"loginLocal": MessageLookupByLibrary.simpleMessage("Local"), ),
"loginLogin": MessageLookupByLibrary.simpleMessage("登录"), "homePageSettingsOtherShelvesDescription":
"loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), MessageLookupByLibrary.simpleMessage("显示所有剩余书架上所有书籍的播放按钮"),
"loginPassword": MessageLookupByLibrary.simpleMessage("密码"), "homePageSettingsQuickPlay":
"loginServerClick": MessageLookupByLibrary.simpleMessage("单击此处"), MessageLookupByLibrary.simpleMessage("继续播放"),
"loginServerConnected": MessageLookupByLibrary.simpleMessage("服务器已连接,请登录"), "homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"),
"loginServerNo": MessageLookupByLibrary.simpleMessage("没有服务器? "), "language": MessageLookupByLibrary.simpleMessage("语言"),
"loginServerNoConnected": MessageLookupByLibrary.simpleMessage( "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"),
"请输入您的AudiobookShelf服务器的URL", "library": MessageLookupByLibrary.simpleMessage("媒体库"),
), "libraryChange": MessageLookupByLibrary.simpleMessage("更改媒体库"),
"loginServerNot": m5, "libraryEmpty": MessageLookupByLibrary.simpleMessage("没有可用的库。"),
"loginServerTo": MessageLookupByLibrary.simpleMessage(" 了解如何设置服务器。"), "libraryLoadError": m4,
"loginTitle": m6, "librarySelect": MessageLookupByLibrary.simpleMessage("选择媒体库"),
"loginToken": MessageLookupByLibrary.simpleMessage("Token"), "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage("切换媒体库"),
"loginUsername": MessageLookupByLibrary.simpleMessage("用户名"), "libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "loading": MessageLookupByLibrary.simpleMessage("加载中..."),
"nmpSettingsBackward": MessageLookupByLibrary.simpleMessage("快退间隔"), "loginLocal": MessageLookupByLibrary.simpleMessage("Local"),
"nmpSettingsForward": MessageLookupByLibrary.simpleMessage("快进间隔"), "loginLogin": MessageLookupByLibrary.simpleMessage("登录"),
"nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage("媒体控制"), "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"),
"nmpSettingsMediaControlsDescription": MessageLookupByLibrary.simpleMessage( "loginPassword": MessageLookupByLibrary.simpleMessage("密码"),
"选择要显示的媒体控件", "loginServerClick": MessageLookupByLibrary.simpleMessage("单击此处"),
), "loginServerConnected":
"nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("服务器已连接,请登录"),
"在下面选择一个字段进行插入", "loginServerNo": MessageLookupByLibrary.simpleMessage("没有服务器? "),
), "loginServerNoConnected": MessageLookupByLibrary.simpleMessage(
"nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage( "请输入您的AudiobookShelf服务器的URL",
"显示章节进度", ),
), "loginServerNot": m5,
"nmpSettingsShowChapterProgressDescription": "loginServerTo": MessageLookupByLibrary.simpleMessage(" 了解如何设置服务器。"),
MessageLookupByLibrary.simpleMessage("而不是本书的整体进展"), "loginTitle": m6,
"nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage("副标题"), "loginToken": MessageLookupByLibrary.simpleMessage("Token"),
"nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( "loginUsername": MessageLookupByLibrary.simpleMessage("用户名"),
"通知的副标题\n", "logs": MessageLookupByLibrary.simpleMessage("日志"),
), "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage("快退间隔"),
"nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("主标题"), "nmpSettingsForward": MessageLookupByLibrary.simpleMessage("快进间隔"),
"nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsMediaControls":
"通知的标题\n", MessageLookupByLibrary.simpleMessage("媒体控制"),
), "nmpSettingsMediaControlsDescription":
"no": MessageLookupByLibrary.simpleMessage(""), MessageLookupByLibrary.simpleMessage(
"notImplemented": MessageLookupByLibrary.simpleMessage("未实现"), "选择要显示的媒体控件",
"notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"), ),
"notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage(
"在通知中自定义媒体播放器", "在下面选择一个字段进行插入",
), ),
"ok": MessageLookupByLibrary.simpleMessage("确定"), "nmpSettingsShowChapterProgress": MessageLookupByLibrary.simpleMessage(
"pause": MessageLookupByLibrary.simpleMessage("暂停"), "显示章节进度",
"play": MessageLookupByLibrary.simpleMessage("播放"), ),
"playerSettings": MessageLookupByLibrary.simpleMessage("播放器设置"), "nmpSettingsShowChapterProgressDescription":
"playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("而不是本书的整体进展"),
"剩余时间标记完成", "nmpSettingsSubTitle": MessageLookupByLibrary.simpleMessage("副标题"),
), "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage(
"playerSettingsCompleteTimeDescriptionHead": "通知的副标题\n",
MessageLookupByLibrary.simpleMessage("当书中剩余时间少于 "), ),
"playerSettingsCompleteTimeDescriptionTail": "nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("主标题"),
MessageLookupByLibrary.simpleMessage(" 时,标记完成"), "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage(
"playerSettingsDescription": MessageLookupByLibrary.simpleMessage( "通知的标题\n",
"自定义播放器设置", ),
), "no": MessageLookupByLibrary.simpleMessage(""),
"playerSettingsDisplay": MessageLookupByLibrary.simpleMessage("显示设置"), "notImplemented": MessageLookupByLibrary.simpleMessage("未实现"),
"playerSettingsDisplayChapterProgress": "notificationMediaPlayer":
MessageLookupByLibrary.simpleMessage("显示章节进度"), MessageLookupByLibrary.simpleMessage("通知媒体播放器"),
"playerSettingsDisplayChapterProgressDescription": "notificationMediaPlayerDescription":
MessageLookupByLibrary.simpleMessage("在播放器中显示当前章节的进度"), MessageLookupByLibrary.simpleMessage(
"playerSettingsDisplayTotalProgress": MessageLookupByLibrary.simpleMessage( "在通知中自定义媒体播放器",
"显示总进度", ),
), "ok": MessageLookupByLibrary.simpleMessage("确定"),
"playerSettingsDisplayTotalProgressDescription": "pause": MessageLookupByLibrary.simpleMessage("暂停"),
MessageLookupByLibrary.simpleMessage("在播放器中显示当前书籍的总进度"), "play": MessageLookupByLibrary.simpleMessage("播放"),
"playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage( "playerSettings": MessageLookupByLibrary.simpleMessage("播放器设置"),
"播放报告间隔", "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage(
), "剩余时间标记完成",
"playerSettingsPlaybackIntervalDescriptionHead": ),
MessageLookupByLibrary.simpleMessage(""), "playerSettingsCompleteTimeDescriptionHead":
"playerSettingsPlaybackIntervalDescriptionTail": MessageLookupByLibrary.simpleMessage("当书中剩余时间少于 "),
MessageLookupByLibrary.simpleMessage(" 向服务器报告一次进度"), "playerSettingsCompleteTimeDescriptionTail":
"playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(" 时,标记完成"),
"回放报告", "playerSettingsDescription": MessageLookupByLibrary.simpleMessage(
), "自定义播放器设置",
"playerSettingsPlaybackReportingIgnore": ),
MessageLookupByLibrary.simpleMessage("忽略播放位置小于"), "playerSettingsDisplay": MessageLookupByLibrary.simpleMessage("显示设置"),
"playerSettingsPlaybackReportingMinimum": "playerSettingsDisplayChapterProgress":
MessageLookupByLibrary.simpleMessage("回放报告最小位置"), MessageLookupByLibrary.simpleMessage("显示章节进度"),
"playerSettingsPlaybackReportingMinimumDescriptionHead": "playerSettingsDisplayChapterProgressDescription":
MessageLookupByLibrary.simpleMessage("不要报告本书前 "), MessageLookupByLibrary.simpleMessage("在播放器中显示当前章节的进度"),
"playerSettingsPlaybackReportingMinimumDescriptionTail": "playerSettingsDisplayTotalProgress":
MessageLookupByLibrary.simpleMessage(" 的播放"), MessageLookupByLibrary.simpleMessage(
"playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage( "显示总进度",
"记住每本书的播放器设置", ),
), "playerSettingsDisplayTotalProgressDescription":
"playerSettingsRememberForEveryBookDescription": MessageLookupByLibrary.simpleMessage("在播放器中显示当前书籍的总进度"),
MessageLookupByLibrary.simpleMessage("每本书都会记住播放速度、音量等设置"), "playerSettingsPlaybackInterval": MessageLookupByLibrary.simpleMessage(
"playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("播放速度"), "播放报告间隔",
"playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage( ),
"默认播放速度", "playerSettingsPlaybackIntervalDescriptionHead":
), MessageLookupByLibrary.simpleMessage(""),
"playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( "playerSettingsPlaybackIntervalDescriptionTail":
"播放速度选项", MessageLookupByLibrary.simpleMessage(" 向服务器报告一次进度"),
), "playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage(
"playerSettingsSpeedOptionsSelect": MessageLookupByLibrary.simpleMessage( "回放报告",
"播放速度选项", ),
), "playerSettingsPlaybackReportingIgnore":
"playerSettingsSpeedOptionsSelectAdd": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("忽略播放位置小于"),
"添加一个速度选项", "playerSettingsPlaybackReportingMinimum":
), MessageLookupByLibrary.simpleMessage("回放报告最小位置"),
"playerSettingsSpeedOptionsSelectAddHelper": "playerSettingsPlaybackReportingMinimumDescriptionHead":
MessageLookupByLibrary.simpleMessage("输入一个新的速度选项"), MessageLookupByLibrary.simpleMessage("不要报告本书前 "),
"playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage("选择播放速度"), "playerSettingsPlaybackReportingMinimumDescriptionTail":
"playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(" 的播放"),
"输入默认的播放速度", "playerSettingsRememberForEveryBook":
), MessageLookupByLibrary.simpleMessage(
"playlistsMine": MessageLookupByLibrary.simpleMessage("播放列表"), "记住每本书的播放器设置",
"readLess": MessageLookupByLibrary.simpleMessage("折叠"), ),
"readMore": MessageLookupByLibrary.simpleMessage("展开"), "playerSettingsRememberForEveryBookDescription":
"refresh": MessageLookupByLibrary.simpleMessage("刷新"), MessageLookupByLibrary.simpleMessage("每本书都会记住播放速度、音量等设置"),
"reset": MessageLookupByLibrary.simpleMessage("重置"), "playerSettingsSpeed": MessageLookupByLibrary.simpleMessage("播放速度"),
"resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"), "playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage(
"resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( "默认播放速度",
"将应用程序设置重置为默认值", ),
), "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage(
"resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( "播放速度选项",
"您确定要重置应用程序设置吗?", ),
), "playerSettingsSpeedOptionsSelect":
"restore": MessageLookupByLibrary.simpleMessage("恢复"), MessageLookupByLibrary.simpleMessage(
"restoreBackup": MessageLookupByLibrary.simpleMessage("恢复备份"), "播放速度选项",
"restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"), ),
"restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"), "playerSettingsSpeedOptionsSelectAdd":
"restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"), MessageLookupByLibrary.simpleMessage(
"restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), "添加一个速度选项",
"restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), ),
"resume": MessageLookupByLibrary.simpleMessage("继续"), "playerSettingsSpeedOptionsSelectAddHelper":
"retry": MessageLookupByLibrary.simpleMessage("重试"), MessageLookupByLibrary.simpleMessage("输入一个新的速度选项"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "playerSettingsSpeedSelect":
"shakeAction": MessageLookupByLibrary.simpleMessage("抖动操作"), MessageLookupByLibrary.simpleMessage("选择播放速度"),
"shakeActionDescription": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage(
"检测到抖动时要执行的操作", "输入默认的播放速度",
), ),
"shakeActivationThreshold": MessageLookupByLibrary.simpleMessage("抖动激活阈值"), "playlistsMine": MessageLookupByLibrary.simpleMessage("播放列表"),
"shakeActivationThresholdDescription": MessageLookupByLibrary.simpleMessage( "readLess": MessageLookupByLibrary.simpleMessage("折叠"),
"门槛越高,你就越难摇晃", "readMore": MessageLookupByLibrary.simpleMessage("展开"),
), "refresh": MessageLookupByLibrary.simpleMessage("刷新"),
"shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), "reset": MessageLookupByLibrary.simpleMessage("重置"),
"shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( "resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"),
"自定义抖动检测器设置", "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage(
), "将应用程序设置重置为默认值",
"shakeDetectorEnable": MessageLookupByLibrary.simpleMessage("启用抖动检测"), ),
"shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage(
"启用抖动检测以执行各种操作", "您确定要重置应用程序设置吗?",
), ),
"shakeDetectorSettings": MessageLookupByLibrary.simpleMessage("抖动检测器设置"), "restore": MessageLookupByLibrary.simpleMessage("恢复"),
"shakeFeedback": MessageLookupByLibrary.simpleMessage("抖动反馈"), "restoreBackup": MessageLookupByLibrary.simpleMessage("恢复备份"),
"shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( "restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"),
"检测到抖动时给出的反馈", "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"),
), "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"),
"shakeSelectAction": MessageLookupByLibrary.simpleMessage("选择抖动动作"), "restoreBackupValidator":
"shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"),
"选择抖动激活阈值", "restoreDescription":
), MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"),
"shakeSelectActivationThresholdHelper": "resume": MessageLookupByLibrary.simpleMessage("继续"),
MessageLookupByLibrary.simpleMessage("输入一个数字以m/s²为单位设置阈值"), "retry": MessageLookupByLibrary.simpleMessage("重试"),
"shakeSelectFeedback": MessageLookupByLibrary.simpleMessage("选择抖动反馈"), "settings": MessageLookupByLibrary.simpleMessage("设置"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "shakeAction": MessageLookupByLibrary.simpleMessage("抖动操作"),
"themeModeDark": MessageLookupByLibrary.simpleMessage("深色"), "shakeActionDescription": MessageLookupByLibrary.simpleMessage(
"themeModeHighContrast": MessageLookupByLibrary.simpleMessage("高对比度模式"), "检测到抖动时要执行的操作",
"themeModeHighContrastDescription": MessageLookupByLibrary.simpleMessage( ),
"增加背景和文本之间的对比度", "shakeActivationThreshold":
), MessageLookupByLibrary.simpleMessage("抖动激活阈值"),
"themeModeLight": MessageLookupByLibrary.simpleMessage("浅色"), "shakeActivationThresholdDescription":
"themeModeSystem": MessageLookupByLibrary.simpleMessage("跟随系统"), MessageLookupByLibrary.simpleMessage(
"themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), "门槛越高,你就越难摇晃",
"themeSettingsColors": MessageLookupByLibrary.simpleMessage("主题色"), ),
"themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage("主题色"), "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"),
"themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage(
"书籍详情页自适应主题", "自定义抖动检测器设置",
), ),
"themeSettingsColorsBookDescription": MessageLookupByLibrary.simpleMessage( "shakeDetectorEnable": MessageLookupByLibrary.simpleMessage("启用抖动检测"),
"以牺牲一些性能为代价,对书籍详情页的颜色进行美化", "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage(
), "启用抖动检测以执行各种操作",
"themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( ),
"根据当前播放的书籍调整主题", "shakeDetectorSettings":
), MessageLookupByLibrary.simpleMessage("抖动检测器设置"),
"themeSettingsColorsCurrentDescription": "shakeFeedback": MessageLookupByLibrary.simpleMessage("抖动反馈"),
MessageLookupByLibrary.simpleMessage("使用当前播放书籍的主题颜色"), "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage(
"themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( "检测到抖动时给出的反馈",
"使用应用程序的系统主题色", ),
), "shakeSelectAction": MessageLookupByLibrary.simpleMessage("选择抖动动作"),
"themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"), "shakeSelectActivationThreshold": MessageLookupByLibrary.simpleMessage(
"timeSecond": m7, "选择抖动激活阈值",
"unknown": MessageLookupByLibrary.simpleMessage("未知"), ),
"webVersion": MessageLookupByLibrary.simpleMessage("Web版本"), "shakeSelectActivationThresholdHelper":
"yes": MessageLookupByLibrary.simpleMessage(""), MessageLookupByLibrary.simpleMessage("输入一个数字以m/s²为单位设置阈值"),
"you": MessageLookupByLibrary.simpleMessage("我的"), "shakeSelectFeedback": MessageLookupByLibrary.simpleMessage("选择抖动反馈"),
"youTooltip": 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("您的个人资料和设置"),
};
} }

View file

@ -489,6 +489,61 @@ class S {
return Intl.message('Downloads', name: 'bookDownloads', desc: '', args: []); 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` /// `Library`
String get library { String get library {
return Intl.message('Library', name: 'library', desc: '', args: []); return Intl.message('Library', name: 'library', desc: '', args: []);

View file

@ -90,6 +90,13 @@
"bookSeries": "Series", "bookSeries": "Series",
"bookDownloads": "Downloads", "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", "library": "Library",
"libraryTooltip": "Browse your library", "libraryTooltip": "Browse your library",
"librarySwitchTooltip": "Switch Library", "librarySwitchTooltip": "Switch Library",

View file

@ -90,6 +90,13 @@
"bookSeries": "系列", "bookSeries": "系列",
"bookDownloads": "下载", "bookDownloads": "下载",
"chapterSelect": "选择章节",
"chapters": "章节列表",
"chapterNotFound": "未找到章节",
"chapterSkip": "跳过章节片头片尾",
"chapterSkipOpen": "跳过章节片头 ",
"chapterSkipEnd": "跳过章节片尾 ",
"library": "媒体库", "library": "媒体库",
"libraryTooltip": "浏览您的媒体库", "libraryTooltip": "浏览您的媒体库",
"librarySwitchTooltip": "切换媒体库", "librarySwitchTooltip": "切换媒体库",

View file

@ -1,24 +1,20 @@
import 'dart:io';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/server_provider.dart'; import 'package:vaani/api/server_provider.dart';
import 'package:vaani/db/storage.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/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/player/providers/audiobook_player.dart';
import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; import 'package:vaani/framework.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/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/models/tray.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/app_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/system_theme_provider.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
import 'package:vaani/theme/theme.dart'; import 'package:vaani/theme/theme.dart';
@ -27,18 +23,8 @@ import 'package:window_manager/window_manager.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// _runPlatformSpecificCode();
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);
});
}
// Configure the App Metadata // Configure the App Metadata
await initialize(); await initialize();
@ -49,16 +35,47 @@ void main() async {
await initStorage(); await initStorage();
// initialize audio player // initialize audio player
await configurePlayer(); // await configurePlayer();
// run the app // run the app
runApp( runApp(
const ProviderScope( const ProviderScope(
child: _EagerInitialization(child: TrayFramework(AbsApp())), child: Framework(
// audioHandler: ,
child: AbsApp(),
),
), ),
); );
} }
Future<void> _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; var routerConfig = const MyAppRouter().config;
class AbsApp extends ConsumerWidget { 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;
}
}

View file

@ -22,7 +22,6 @@ class _TrayFrameworkState extends ConsumerState<TrayFramework>
windowManager.addListener(this); windowManager.addListener(this);
_init(); _init();
} }
super.initState(); super.initState();
} }

View file

@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider;
import 'package:vaani/features/explore/providers/search_controller.dart'; import 'package:vaani/features/explore/providers/search_controller.dart';
import 'package:vaani/features/player/providers/player_form.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/player/view/player_minimized.dart';
import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
@ -53,9 +54,10 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
} }
Widget buildNavLeft(BuildContext context, WidgetRef ref) { Widget buildNavLeft(BuildContext context, WidgetRef ref) {
final isPlayerActive = ref.watch(isPlayerActiveProvider); // final isPlayerActive = ref.watch(isPlayerActiveProvider);
final session = ref.watch(sessionProvider);
return Padding( return Padding(
padding: EdgeInsets.only(bottom: isPlayerActive ? playerMinHeight : 0), padding: EdgeInsets.only(bottom: session != null ? playerMinHeight : 0),
child: Row( child: Row(
children: [ children: [
SafeArea( SafeArea(
@ -80,9 +82,11 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
? libraryIcon ?? item.activeIcon ? libraryIcon ?? item.activeIcon
: item.activeIcon, : item.activeIcon,
), ),
label: Text(isDestinationLibrary label: Text(
? currentLibrary?.name ?? item.name isDestinationLibrary
: item.name), ? currentLibrary?.name ?? item.name
: item.name,
),
// tooltip: item.tooltip, // tooltip: item.tooltip,
); );
// if (isDestinationLibrary) { // if (isDestinationLibrary) {
@ -101,7 +105,6 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
}).toList(), }).toList(),
selectedIndex: navigationShell.currentIndex, selectedIndex: navigationShell.currentIndex,
onDestinationSelected: (int index) { onDestinationSelected: (int index) {
print(index);
_onTap(context, index, ref); _onTap(context, index, ref);
}, },
), ),
@ -121,7 +124,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
// final playerMaxHeight = size.height; // final playerMaxHeight = size.height;
// var percentExpandedMiniPlayer = (playerProgress - playerMinHeight) / // var percentExpandedMiniPlayer = (playerProgress - playerMinHeight) /
// (playerMaxHeight - playerMinHeight); // (playerMaxHeight - playerMinHeight);
// Clamp the value between 0 and 1 // // Clamp the value between 0 and 1
// percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0); // percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0);
return NavigationBar( return NavigationBar(
elevation: 0.0, elevation: 0.0,

View file

@ -11,7 +11,8 @@ import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider;
import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/item_viewer/view/library_item_actions.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/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/models/library_item_extras.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
@ -212,10 +213,13 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final me = ref.watch(meProvider); final me = ref.watch(meProvider);
final player = ref.watch(audiobookPlayerProvider); // final player = ref.watch(audiobookPlayerProvider);
final isCurrentBookSetInPlayer = final session = ref.watch(sessionProvider);
player.book?.libraryItemId == libraryItemId; final playerStatus = ref.watch(playerStatusProvider);
final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; final isLoading = playerStatus.isLoading(libraryItemId);
final isCurrentBookSetInPlayer = session?.libraryItemId == libraryItemId;
final isPlayingThisBook =
playerStatus.isPlaying() && isCurrentBookSetInPlayer;
final userProgress = me.valueOrNull?.mediaProgress final userProgress = me.valueOrNull?.mediaProgress
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId); ?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
@ -285,19 +289,15 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
.withValues(alpha: 0.9), .withValues(alpha: 0.9),
), ),
), ),
onPressed: () async { onPressed: () => session?.libraryItemId == libraryItemId
final book = ? ref.read(playerProvider).togglePlayPause()
await ref.watch(libraryItemProvider(libraryItemId).future); : ref
.read(sessionProvider.notifier)
libraryItemPlayButtonOnPressed( .load(libraryItemId, null),
ref: ref,
book: book.media.asBookExpanded,
userMediaProgress: userProgress,
);
},
icon: Hero( icon: Hero(
tag: HeroTagPrefixes.libraryItemPlayButton + libraryItemId, tag: HeroTagPrefixes.libraryItemPlayButton + libraryItemId,
child: DynamicItemPlayIcon( child: DynamicItemPlayIcon(
isLoading: isLoading,
isBookCompleted: isBookCompleted, isBookCompleted: isBookCompleted,
isPlayingThisBook: isPlayingThisBook, isPlayingThisBook: isPlayingThisBook,
isCurrentBookSetInPlayer: isCurrentBookSetInPlayer, isCurrentBookSetInPlayer: isCurrentBookSetInPlayer,
@ -336,3 +336,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);
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();
}
}

View file

@ -318,6 +318,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" 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: custom_lint:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -520,6 +528,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -762,15 +778,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.10.5" 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: just_audio_media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
@ -795,14 +802,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.16" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -915,6 +914,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" 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: menu_base:
dependency: transitive dependency: transitive
description: description:

View file

@ -22,7 +22,7 @@ environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
flutter: 3.32.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. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@ -42,7 +42,8 @@ dependencies:
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
coast: ^2.0.2 coast: ^2.0.2
collection: ^1.18.0 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 device_info_plus: ^11.3.3
duration_picker: ^1.2.0 duration_picker: ^1.2.0
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
@ -57,22 +58,22 @@ dependencies:
# font_awesome_flutter: ^10.7.0 # font_awesome_flutter: ^10.7.0
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
go_router: ^14.0.2 go_router: ^14.0.2
hive: ^4.0.0-dev.2 hive: ^4.0.0-dev.2
hooks_riverpod: ^2.5.1 hooks_riverpod: ^2.5.1
isar: ^4.0.0-dev.13 isar: ^4.0.0-dev.14
isar_flutter_libs: ^4.0.0-dev.13 isar_flutter_libs: ^4.0.0-dev.14
json_annotation: ^4.9.0 json_annotation: ^4.9.0
just_audio: ^0.10.5 just_audio: ^0.10.5
just_audio_background: # just_audio_background:
# TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed # # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed
git: # git:
url: https://github.com/Dr-Blank/just_audio # url: https://github.com/Dr-Blank/just_audio
ref: media-notification-config # ref: media-notification-config
path: just_audio_background # path: just_audio_background
just_audio_windows: ^0.2.2 # just_audio_windows: ^0.2.2
just_audio_media_kit: ^2.0.4 just_audio_media_kit: ^2.0.4
media_kit_libs_linux: any 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 list_wheel_scroll_view_nls: ^0.0.3
logging: ^1.2.0 logging: ^1.2.0
logging_appenders: ^1.3.1 logging_appenders: ^1.3.1

View file

@ -8,7 +8,7 @@
#include <dynamic_color/dynamic_color_plugin_c_api.h> #include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h> #include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <just_audio_windows/just_audio_windows_plugin.h> #include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
@ -21,8 +21,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
IsarFlutterLibsPluginRegisterWithRegistrar( IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
JustAudioWindowsPluginRegisterWithRegistrar( MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("JustAudioWindowsPlugin")); registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(

View file

@ -5,7 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
isar_flutter_libs isar_flutter_libs
just_audio_windows media_kit_libs_windows_audio
permission_handler_windows permission_handler_windows
screen_retriever_windows screen_retriever_windows
share_plus share_plus