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,15 +496,25 @@ 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
? SizedBox(
// width: 20,
// height: 20,
child: CircularProgressIndicator(
strokeWidth: 4,
),
)
: Icon(
isCurrentBookSetInPlayer isCurrentBookSetInPlayer
? isPlayingThisBook ? isPlayingThisBook
? Icons.pause_rounded ? Icons.pause_rounded
@ -512,68 +525,3 @@ class DynamicItemPlayIcon extends StatelessWidget {
); );
} }
} }
/// 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) {
audioSources.add(
AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token), _getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
tag: MediaItem(
id: '${audiobook.libraryItemId}${track.index}',
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,
artUri: Uri.parse(
'$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token',
), ),
) ),
.toList(); );
final track = playbackSession.findTrackAtTime(playbackSession.currentTime);
queue.add(mediaItems); 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;
} }
return _book!.findChapterAtTime(positionInBook);
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();
}
_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,8 +148,7 @@ class PlayerExpanded extends HookConsumerWidget {
), ),
), ),
Expanded( SizedBox(
child: SizedBox(
width: imageSize, width: imageSize,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -173,7 +158,6 @@ class PlayerExpanded extends HookConsumerWidget {
child: const AudiobookProgressBar(), child: const AudiobookProgressBar(),
), ),
), ),
),
// the chapter skip buttons, seek 30 seconds back and forward, and play/pause button // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
Expanded( Expanded(

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 {
playPauseController.reverse();
} }
return switch (player.processingState) {
ProcessingState.loading || ProcessingState.buffering => const Padding( Widget _getIcon(PlayStatus playerStatus, BuildContext context) {
padding: EdgeInsets.all(AppElementSizes.paddingRegular), switch (playerStatus) {
child: CircularProgressIndicator(), case PlayStatus.playing:
), return Icon(size: iconSize, PlatformIcons(context).pause);
ProcessingState.completed => IconButton( case PlayStatus.paused:
onPressed: () async { 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.seekInBook(const Duration(seconds: 0));
await player.play(); await player.play();
}, break;
icon: const Icon( default:
Icons.replay, await player.play();
), }
),
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(
player.positionStreamInChapter.listen((positionChapter) {
if (end >
(player.currentChapter?.duration ?? Duration.zero) -
positionChapter) {
Future.microtask(
() => throttlerEnd.call(() => player.skipToNext()),
);
} }
}),
void skipStart(BookChapter chapter) { );
print('跳过片头');
final globalPosition = player.positionInBook;
if (globalPosition - chapter.start < Duration(seconds: 1)) {
player.seekInBook(chapter.start + start);
}
}
void skipEnd(chapter) {
print('跳过片尾');
final book = player.book;
if (book == null) {
return;
}
if (start > Duration.zero) {
final currentIndex = book.chapters.indexOf(chapter);
if (currentIndex < book.chapters.length - 1) {
final nextChapter = book.chapters[currentIndex + 1];
// +
print('跳过片头+片尾');
player.skipToChapter(nextChapter.id, position: start);
}
} else {
player.seekToPrevious();
} }
} }
@ -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

@ -54,8 +54,10 @@ class MessageLookup extends MessageLookupByLibrary {
"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"),
"accountManage":
MessageLookupByLibrary.simpleMessage("Manage Accounts"),
"accountRegisteredServers": MessageLookupByLibrary.simpleMessage( "accountRegisteredServers": MessageLookupByLibrary.simpleMessage(
"Registered Servers", "Registered Servers",
), ),
@ -94,7 +96,8 @@ class MessageLookup extends MessageLookupByLibrary {
"autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage( "autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage(
"Always Auto Turn On Timer", "Always Auto Turn On Timer",
), ),
"autoTurnOnTimerAlwaysDescription": MessageLookupByLibrary.simpleMessage( "autoTurnOnTimerAlwaysDescription":
MessageLookupByLibrary.simpleMessage(
"Always turn on the sleep timer, no matter what", "Always turn on the sleep timer, no matter what",
), ),
"autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage(
@ -122,9 +125,11 @@ class MessageLookup extends MessageLookupByLibrary {
"bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"), "bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"),
"bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"), "bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"),
"bookGenres": MessageLookupByLibrary.simpleMessage("Genres"), "bookGenres": MessageLookupByLibrary.simpleMessage("Genres"),
"bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"), "bookMetadataAbridged":
MessageLookupByLibrary.simpleMessage("Abridged"),
"bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"),
"bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"), "bookMetadataPublished":
MessageLookupByLibrary.simpleMessage("Published"),
"bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage(
"Unabridged", "Unabridged",
), ),
@ -134,6 +139,18 @@ class MessageLookup extends MessageLookupByLibrary {
"No shelves to display", "No shelves to display",
), ),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"chapterNotFound": MessageLookupByLibrary.simpleMessage("Chapters"),
"chapterSelect": MessageLookupByLibrary.simpleMessage("Select Chapter"),
"chapterSkip": MessageLookupByLibrary.simpleMessage(
"Skip chapter opening and ending",
),
"chapterSkipEnd": MessageLookupByLibrary.simpleMessage(
"Skip chapter opening for ",
),
"chapterSkipOpen": MessageLookupByLibrary.simpleMessage(
"Skip chapter opening for ",
),
"chapters": MessageLookupByLibrary.simpleMessage("Chapters"),
"copyToClipboard": MessageLookupByLibrary.simpleMessage( "copyToClipboard": MessageLookupByLibrary.simpleMessage(
"Copy to Clipboard", "Copy to Clipboard",
), ),
@ -166,11 +183,13 @@ class MessageLookup extends MessageLookupByLibrary {
"homeBookContinueSeries": MessageLookupByLibrary.simpleMessage( "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage(
"Continue Series", "Continue Series",
), ),
"homeBookContinueSeriesDescription": MessageLookupByLibrary.simpleMessage( "homeBookContinueSeriesDescription":
MessageLookupByLibrary.simpleMessage(
"Show play button for books in continue series shelf", "Show play button for books in continue series shelf",
), ),
"homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"), "homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"),
"homeBookListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), "homeBookListenAgain":
MessageLookupByLibrary.simpleMessage("Listen Again"),
"homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage( "homeBookListenAgainDescription": MessageLookupByLibrary.simpleMessage(
"Show play button for all books in listen again shelf", "Show play button for all books in listen again shelf",
), ),
@ -180,7 +199,8 @@ class MessageLookup extends MessageLookupByLibrary {
"homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage( "homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage(
"Recently Added", "Recently Added",
), ),
"homeBookRecommended": MessageLookupByLibrary.simpleMessage("Recommended"), "homeBookRecommended":
MessageLookupByLibrary.simpleMessage("Recommended"),
"homeContinueListening": MessageLookupByLibrary.simpleMessage( "homeContinueListening": MessageLookupByLibrary.simpleMessage(
"Continue Listening", "Continue Listening",
), ),
@ -253,7 +273,8 @@ class MessageLookup extends MessageLookupByLibrary {
"nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage( "nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage(
"Media Controls", "Media Controls",
), ),
"nmpSettingsMediaControlsDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsMediaControlsDescription":
MessageLookupByLibrary.simpleMessage(
"Select the media controls to display", "Select the media controls to display",
), ),
"nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage(
@ -272,27 +293,32 @@ class MessageLookup extends MessageLookupByLibrary {
"nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsSubTitleDescription": MessageLookupByLibrary.simpleMessage(
"The subtitle of the notification\n", "The subtitle of the notification\n",
), ),
"nmpSettingsTitle": MessageLookupByLibrary.simpleMessage("Primary Title"), "nmpSettingsTitle":
MessageLookupByLibrary.simpleMessage("Primary Title"),
"nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage( "nmpSettingsTitleDescription": MessageLookupByLibrary.simpleMessage(
"The title of the notification\n", "The title of the notification\n",
), ),
"no": MessageLookupByLibrary.simpleMessage("No"), "no": MessageLookupByLibrary.simpleMessage("No"),
"notImplemented": MessageLookupByLibrary.simpleMessage("Not implemented"), "notImplemented":
MessageLookupByLibrary.simpleMessage("Not implemented"),
"notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage(
"Notification Media Player", "Notification Media Player",
), ),
"notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( "notificationMediaPlayerDescription":
MessageLookupByLibrary.simpleMessage(
"Customize the media player in notifications", "Customize the media player in notifications",
), ),
"ok": MessageLookupByLibrary.simpleMessage("OK"), "ok": MessageLookupByLibrary.simpleMessage("OK"),
"pause": MessageLookupByLibrary.simpleMessage("Pause"), "pause": MessageLookupByLibrary.simpleMessage("Pause"),
"play": MessageLookupByLibrary.simpleMessage("Play"), "play": MessageLookupByLibrary.simpleMessage("Play"),
"playerSettings": MessageLookupByLibrary.simpleMessage("Player Settings"), "playerSettings":
MessageLookupByLibrary.simpleMessage("Player Settings"),
"playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage( "playerSettingsCompleteTime": MessageLookupByLibrary.simpleMessage(
"Mark Complete When Time Left", "Mark Complete When Time Left",
), ),
"playerSettingsCompleteTimeDescriptionHead": "playerSettingsCompleteTimeDescriptionHead":
MessageLookupByLibrary.simpleMessage("Mark complete when less than "), MessageLookupByLibrary.simpleMessage(
"Mark complete when less than "),
"playerSettingsCompleteTimeDescriptionTail": "playerSettingsCompleteTimeDescriptionTail":
MessageLookupByLibrary.simpleMessage(" left in the book"), MessageLookupByLibrary.simpleMessage(" left in the book"),
"playerSettingsDescription": MessageLookupByLibrary.simpleMessage( "playerSettingsDescription": MessageLookupByLibrary.simpleMessage(
@ -307,7 +333,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage( 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":
MessageLookupByLibrary.simpleMessage(
"Show Total Progress", "Show Total Progress",
), ),
"playerSettingsDisplayTotalProgressDescription": "playerSettingsDisplayTotalProgressDescription":
@ -336,7 +363,8 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"playerSettingsPlaybackReportingMinimumDescriptionTail": "playerSettingsPlaybackReportingMinimumDescriptionTail":
MessageLookupByLibrary.simpleMessage("of the book"), MessageLookupByLibrary.simpleMessage("of the book"),
"playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage( "playerSettingsRememberForEveryBook":
MessageLookupByLibrary.simpleMessage(
"Remember Player Settings for Every Book", "Remember Player Settings for Every Book",
), ),
"playerSettingsRememberForEveryBookDescription": "playerSettingsRememberForEveryBookDescription":
@ -350,14 +378,17 @@ class MessageLookup extends MessageLookupByLibrary {
"playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage(
"Speed Options", "Speed Options",
), ),
"playerSettingsSpeedOptionsSelect": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptionsSelect":
MessageLookupByLibrary.simpleMessage(
"Select Speed Options", "Select Speed Options",
), ),
"playerSettingsSpeedOptionsSelectAdd": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptionsSelectAdd":
MessageLookupByLibrary.simpleMessage(
"Add Speed Option", "Add Speed Option",
), ),
"playerSettingsSpeedOptionsSelectAddHelper": "playerSettingsSpeedOptionsSelectAddHelper":
MessageLookupByLibrary.simpleMessage("Enter a new speed option to add"), MessageLookupByLibrary.simpleMessage(
"Enter a new speed option to add"),
"playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage(
"Select Speed", "Select Speed",
), ),
@ -405,7 +436,8 @@ class MessageLookup extends MessageLookupByLibrary {
"shakeActivationThreshold": MessageLookupByLibrary.simpleMessage( "shakeActivationThreshold": MessageLookupByLibrary.simpleMessage(
"Shake Activation Threshold", "Shake Activation Threshold",
), ),
"shakeActivationThresholdDescription": MessageLookupByLibrary.simpleMessage( "shakeActivationThresholdDescription":
MessageLookupByLibrary.simpleMessage(
"The higher the threshold, the harder you need to shake", "The higher the threshold, the harder you need to shake",
), ),
"shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"),
@ -443,7 +475,8 @@ class MessageLookup extends MessageLookupByLibrary {
"themeModeHighContrast": MessageLookupByLibrary.simpleMessage( "themeModeHighContrast": MessageLookupByLibrary.simpleMessage(
"High Contrast Mode", "High Contrast Mode",
), ),
"themeModeHighContrastDescription": MessageLookupByLibrary.simpleMessage( "themeModeHighContrastDescription":
MessageLookupByLibrary.simpleMessage(
"Increase the contrast between the background and the text", "Increase the contrast between the background and the text",
), ),
"themeModeLight": MessageLookupByLibrary.simpleMessage("Light"), "themeModeLight": MessageLookupByLibrary.simpleMessage("Light"),
@ -458,7 +491,8 @@ class MessageLookup extends MessageLookupByLibrary {
"themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage(
"Adaptive Theme on Item Page", "Adaptive Theme on Item Page",
), ),
"themeSettingsColorsBookDescription": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsBookDescription":
MessageLookupByLibrary.simpleMessage(
"Get fancy with the colors on the item page at the cost of some performance", "Get fancy with the colors on the item page at the cost of some performance",
), ),
"themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage(

View file

@ -50,7 +50,8 @@ class MessageLookup extends MessageLookupByLibrary {
"accountDeleteServer": MessageLookupByLibrary.simpleMessage("删除服务器"), "accountDeleteServer": MessageLookupByLibrary.simpleMessage("删除服务器"),
"accountInvalidURL": MessageLookupByLibrary.simpleMessage("无效网址"), "accountInvalidURL": MessageLookupByLibrary.simpleMessage("无效网址"),
"accountManage": MessageLookupByLibrary.simpleMessage("帐户管理"), "accountManage": MessageLookupByLibrary.simpleMessage("帐户管理"),
"accountRegisteredServers": MessageLookupByLibrary.simpleMessage("已注册服务器"), "accountRegisteredServers":
MessageLookupByLibrary.simpleMessage("已注册服务器"),
"accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage( "accountRemoveServerAndUsers": MessageLookupByLibrary.simpleMessage(
"删除服务器和用户", "删除服务器和用户",
), ),
@ -60,7 +61,8 @@ class MessageLookup extends MessageLookupByLibrary {
"accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage( "accountRemoveServerAndUsersTail": MessageLookupByLibrary.simpleMessage(
" 以及该应用程序中所有用户的登录信息。", " 以及该应用程序中所有用户的登录信息。",
), ),
"accountRemoveUserLogin": MessageLookupByLibrary.simpleMessage("删除用户登录"), "accountRemoveUserLogin":
MessageLookupByLibrary.simpleMessage("删除用户登录"),
"accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage( "accountRemoveUserLoginHead": MessageLookupByLibrary.simpleMessage(
"这将删除用户 ", "这将删除用户 ",
), ),
@ -72,11 +74,15 @@ class MessageLookup extends MessageLookupByLibrary {
"accountUsersCount": m1, "accountUsersCount": m1,
"appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"),
"appearance": MessageLookupByLibrary.simpleMessage("外观"), "appearance": MessageLookupByLibrary.simpleMessage("外观"),
"autoSleepTimerSettings": MessageLookupByLibrary.simpleMessage("自动睡眠定时器设置"), "autoSleepTimerSettings":
"autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), MessageLookupByLibrary.simpleMessage("自动睡眠定时器设置"),
"autoTurnOnSleepTimer":
MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"),
"autoTurnOnTimer": MessageLookupByLibrary.simpleMessage("自动开启定时器"), "autoTurnOnTimer": MessageLookupByLibrary.simpleMessage("自动开启定时器"),
"autoTurnOnTimerAlways": MessageLookupByLibrary.simpleMessage("始终自动开启定时器"), "autoTurnOnTimerAlways":
"autoTurnOnTimerAlwaysDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("始终自动开启定时器"),
"autoTurnOnTimerAlwaysDescription":
MessageLookupByLibrary.simpleMessage(
"总是打开睡眠定时器", "总是打开睡眠定时器",
), ),
"autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage( "autoTurnOnTimerDescription": MessageLookupByLibrary.simpleMessage(
@ -108,11 +114,18 @@ class MessageLookup extends MessageLookupByLibrary {
"bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"), "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"),
"bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"), "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"), "cancel": MessageLookupByLibrary.simpleMessage("取消"),
"chapterNotFound": MessageLookupByLibrary.simpleMessage("未找到章节"),
"chapterSelect": MessageLookupByLibrary.simpleMessage("选择章节"),
"chapterSkip": MessageLookupByLibrary.simpleMessage("跳过章节片头片尾"),
"chapterSkipEnd": MessageLookupByLibrary.simpleMessage("跳过章节片尾 "),
"chapterSkipOpen": MessageLookupByLibrary.simpleMessage("跳过章节片头 "),
"chapters": MessageLookupByLibrary.simpleMessage("章节列表"),
"copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"),
"copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage(
"将应用程序设置复制到剪贴板", "将应用程序设置复制到剪贴板",
), ),
"copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), "copyToClipboardToast":
MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteDialog": m2, "deleteDialog": m2,
"deleted": m3, "deleted": m3,
@ -122,11 +135,13 @@ class MessageLookup extends MessageLookupByLibrary {
"general": MessageLookupByLibrary.simpleMessage("通用"), "general": MessageLookupByLibrary.simpleMessage("通用"),
"help": MessageLookupByLibrary.simpleMessage("Help"), "help": MessageLookupByLibrary.simpleMessage("Help"),
"home": MessageLookupByLibrary.simpleMessage("首页"), "home": MessageLookupByLibrary.simpleMessage("首页"),
"homeBookContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), "homeBookContinueListening":
MessageLookupByLibrary.simpleMessage("继续收听"),
"homeBookContinueListeningDescription": "homeBookContinueListeningDescription":
MessageLookupByLibrary.simpleMessage("继续收听书架上显示播放按钮"), MessageLookupByLibrary.simpleMessage("继续收听书架上显示播放按钮"),
"homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"), "homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"),
"homeBookContinueSeriesDescription": MessageLookupByLibrary.simpleMessage( "homeBookContinueSeriesDescription":
MessageLookupByLibrary.simpleMessage(
"继续系列书架上显示播放按钮", "继续系列书架上显示播放按钮",
), ),
"homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"), "homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"),
@ -148,7 +163,8 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"homePageSettingsOtherShelvesDescription": "homePageSettingsOtherShelvesDescription":
MessageLookupByLibrary.simpleMessage("显示所有剩余书架上所有书籍的播放按钮"), MessageLookupByLibrary.simpleMessage("显示所有剩余书架上所有书籍的播放按钮"),
"homePageSettingsQuickPlay": MessageLookupByLibrary.simpleMessage("继续播放"), "homePageSettingsQuickPlay":
MessageLookupByLibrary.simpleMessage("继续播放"),
"homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"), "homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"),
@ -165,7 +181,8 @@ class MessageLookup extends MessageLookupByLibrary {
"loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"), "loginOpenID": MessageLookupByLibrary.simpleMessage("OpenID"),
"loginPassword": MessageLookupByLibrary.simpleMessage("密码"), "loginPassword": MessageLookupByLibrary.simpleMessage("密码"),
"loginServerClick": MessageLookupByLibrary.simpleMessage("单击此处"), "loginServerClick": MessageLookupByLibrary.simpleMessage("单击此处"),
"loginServerConnected": MessageLookupByLibrary.simpleMessage("服务器已连接,请登录"), "loginServerConnected":
MessageLookupByLibrary.simpleMessage("服务器已连接,请登录"),
"loginServerNo": MessageLookupByLibrary.simpleMessage("没有服务器? "), "loginServerNo": MessageLookupByLibrary.simpleMessage("没有服务器? "),
"loginServerNoConnected": MessageLookupByLibrary.simpleMessage( "loginServerNoConnected": MessageLookupByLibrary.simpleMessage(
"请输入您的AudiobookShelf服务器的URL", "请输入您的AudiobookShelf服务器的URL",
@ -178,8 +195,10 @@ class MessageLookup extends MessageLookupByLibrary {
"logs": MessageLookupByLibrary.simpleMessage("日志"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"nmpSettingsBackward": MessageLookupByLibrary.simpleMessage("快退间隔"), "nmpSettingsBackward": MessageLookupByLibrary.simpleMessage("快退间隔"),
"nmpSettingsForward": MessageLookupByLibrary.simpleMessage("快进间隔"), "nmpSettingsForward": MessageLookupByLibrary.simpleMessage("快进间隔"),
"nmpSettingsMediaControls": MessageLookupByLibrary.simpleMessage("媒体控制"), "nmpSettingsMediaControls":
"nmpSettingsMediaControlsDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("媒体控制"),
"nmpSettingsMediaControlsDescription":
MessageLookupByLibrary.simpleMessage(
"选择要显示的媒体控件", "选择要显示的媒体控件",
), ),
"nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage( "nmpSettingsSelectOne": MessageLookupByLibrary.simpleMessage(
@ -200,8 +219,10 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"no": MessageLookupByLibrary.simpleMessage(""), "no": MessageLookupByLibrary.simpleMessage(""),
"notImplemented": MessageLookupByLibrary.simpleMessage("未实现"), "notImplemented": MessageLookupByLibrary.simpleMessage("未实现"),
"notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"), "notificationMediaPlayer":
"notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("通知媒体播放器"),
"notificationMediaPlayerDescription":
MessageLookupByLibrary.simpleMessage(
"在通知中自定义媒体播放器", "在通知中自定义媒体播放器",
), ),
"ok": MessageLookupByLibrary.simpleMessage("确定"), "ok": MessageLookupByLibrary.simpleMessage("确定"),
@ -223,7 +244,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("显示章节进度"), MessageLookupByLibrary.simpleMessage("显示章节进度"),
"playerSettingsDisplayChapterProgressDescription": "playerSettingsDisplayChapterProgressDescription":
MessageLookupByLibrary.simpleMessage("在播放器中显示当前章节的进度"), MessageLookupByLibrary.simpleMessage("在播放器中显示当前章节的进度"),
"playerSettingsDisplayTotalProgress": MessageLookupByLibrary.simpleMessage( "playerSettingsDisplayTotalProgress":
MessageLookupByLibrary.simpleMessage(
"显示总进度", "显示总进度",
), ),
"playerSettingsDisplayTotalProgressDescription": "playerSettingsDisplayTotalProgressDescription":
@ -246,7 +268,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("不要报告本书前 "), MessageLookupByLibrary.simpleMessage("不要报告本书前 "),
"playerSettingsPlaybackReportingMinimumDescriptionTail": "playerSettingsPlaybackReportingMinimumDescriptionTail":
MessageLookupByLibrary.simpleMessage(" 的播放"), MessageLookupByLibrary.simpleMessage(" 的播放"),
"playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage( "playerSettingsRememberForEveryBook":
MessageLookupByLibrary.simpleMessage(
"记住每本书的播放器设置", "记住每本书的播放器设置",
), ),
"playerSettingsRememberForEveryBookDescription": "playerSettingsRememberForEveryBookDescription":
@ -258,15 +281,18 @@ class MessageLookup extends MessageLookupByLibrary {
"playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage(
"播放速度选项", "播放速度选项",
), ),
"playerSettingsSpeedOptionsSelect": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptionsSelect":
MessageLookupByLibrary.simpleMessage(
"播放速度选项", "播放速度选项",
), ),
"playerSettingsSpeedOptionsSelectAdd": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedOptionsSelectAdd":
MessageLookupByLibrary.simpleMessage(
"添加一个速度选项", "添加一个速度选项",
), ),
"playerSettingsSpeedOptionsSelectAddHelper": "playerSettingsSpeedOptionsSelectAddHelper":
MessageLookupByLibrary.simpleMessage("输入一个新的速度选项"), MessageLookupByLibrary.simpleMessage("输入一个新的速度选项"),
"playerSettingsSpeedSelect": MessageLookupByLibrary.simpleMessage("选择播放速度"), "playerSettingsSpeedSelect":
MessageLookupByLibrary.simpleMessage("选择播放速度"),
"playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage( "playerSettingsSpeedSelectHelper": MessageLookupByLibrary.simpleMessage(
"输入默认的播放速度", "输入默认的播放速度",
), ),
@ -287,8 +313,10 @@ class MessageLookup extends MessageLookupByLibrary {
"restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"), "restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"),
"restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"), "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"),
"restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"), "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"),
"restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), "restoreBackupValidator":
"restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"),
"restoreDescription":
MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"),
"resume": MessageLookupByLibrary.simpleMessage("继续"), "resume": MessageLookupByLibrary.simpleMessage("继续"),
"retry": MessageLookupByLibrary.simpleMessage("重试"), "retry": MessageLookupByLibrary.simpleMessage("重试"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "settings": MessageLookupByLibrary.simpleMessage("设置"),
@ -296,8 +324,10 @@ class MessageLookup extends MessageLookupByLibrary {
"shakeActionDescription": MessageLookupByLibrary.simpleMessage( "shakeActionDescription": MessageLookupByLibrary.simpleMessage(
"检测到抖动时要执行的操作", "检测到抖动时要执行的操作",
), ),
"shakeActivationThreshold": MessageLookupByLibrary.simpleMessage("抖动激活阈值"), "shakeActivationThreshold":
"shakeActivationThresholdDescription": MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage("抖动激活阈值"),
"shakeActivationThresholdDescription":
MessageLookupByLibrary.simpleMessage(
"门槛越高,你就越难摇晃", "门槛越高,你就越难摇晃",
), ),
"shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"),
@ -308,7 +338,8 @@ class MessageLookup extends MessageLookupByLibrary {
"shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage( "shakeDetectorEnableDescription": MessageLookupByLibrary.simpleMessage(
"启用抖动检测以执行各种操作", "启用抖动检测以执行各种操作",
), ),
"shakeDetectorSettings": MessageLookupByLibrary.simpleMessage("抖动检测器设置"), "shakeDetectorSettings":
MessageLookupByLibrary.simpleMessage("抖动检测器设置"),
"shakeFeedback": MessageLookupByLibrary.simpleMessage("抖动反馈"), "shakeFeedback": MessageLookupByLibrary.simpleMessage("抖动反馈"),
"shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage( "shakeFeedbackDescription": MessageLookupByLibrary.simpleMessage(
"检测到抖动时给出的反馈", "检测到抖动时给出的反馈",
@ -323,18 +354,21 @@ class MessageLookup extends MessageLookupByLibrary {
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"themeModeDark": MessageLookupByLibrary.simpleMessage("深色"), "themeModeDark": MessageLookupByLibrary.simpleMessage("深色"),
"themeModeHighContrast": MessageLookupByLibrary.simpleMessage("高对比度模式"), "themeModeHighContrast": MessageLookupByLibrary.simpleMessage("高对比度模式"),
"themeModeHighContrastDescription": MessageLookupByLibrary.simpleMessage( "themeModeHighContrastDescription":
MessageLookupByLibrary.simpleMessage(
"增加背景和文本之间的对比度", "增加背景和文本之间的对比度",
), ),
"themeModeLight": MessageLookupByLibrary.simpleMessage("浅色"), "themeModeLight": MessageLookupByLibrary.simpleMessage("浅色"),
"themeModeSystem": MessageLookupByLibrary.simpleMessage("跟随系统"), "themeModeSystem": MessageLookupByLibrary.simpleMessage("跟随系统"),
"themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), "themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"),
"themeSettingsColors": MessageLookupByLibrary.simpleMessage("主题色"), "themeSettingsColors": MessageLookupByLibrary.simpleMessage("主题色"),
"themeSettingsColorsAndroid": MessageLookupByLibrary.simpleMessage("主题色"), "themeSettingsColorsAndroid":
MessageLookupByLibrary.simpleMessage("主题色"),
"themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsBook": MessageLookupByLibrary.simpleMessage(
"书籍详情页自适应主题", "书籍详情页自适应主题",
), ),
"themeSettingsColorsBookDescription": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsBookDescription":
MessageLookupByLibrary.simpleMessage(
"以牺牲一些性能为代价,对书籍详情页的颜色进行美化", "以牺牲一些性能为代价,对书籍详情页的颜色进行美化",
), ),
"themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsCurrent": MessageLookupByLibrary.simpleMessage(
@ -345,7 +379,8 @@ class MessageLookup extends MessageLookupByLibrary {
"themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage( "themeSettingsColorsDescription": MessageLookupByLibrary.simpleMessage(
"使用应用程序的系统主题色", "使用应用程序的系统主题色",
), ),
"themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"), "themeSettingsDescription":
MessageLookupByLibrary.simpleMessage("自定义应用主题"),
"timeSecond": m7, "timeSecond": m7,
"unknown": MessageLookupByLibrary.simpleMessage("未知"), "unknown": MessageLookupByLibrary.simpleMessage("未知"),
"webVersion": MessageLookupByLibrary.simpleMessage("Web版本"), "webVersion": MessageLookupByLibrary.simpleMessage("Web版本"),

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(
isDestinationLibrary
? currentLibrary?.name ?? item.name ? currentLibrary?.name ?? item.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
@ -59,20 +60,20 @@ dependencies:
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