替换miniPlayer

This commit is contained in:
rang 2025-11-13 17:53:23 +08:00
parent eb9b8f3b94
commit e67d045da6
34 changed files with 1777 additions and 1078 deletions

View file

@ -12,4 +12,6 @@ class AppElementSizes {
static const double iconSizeRegular = 48.0; static const double iconSizeRegular = 48.0;
static const double iconSizeSmall = 36.0; static const double iconSizeSmall = 36.0;
static const double iconSizeLarge = 64.0; static const double iconSizeLarge = 64.0;
static const double barHeight = 3.0;
} }

View file

@ -8,13 +8,19 @@ 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';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
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';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
final _logger = Logger('AudiobookPlayer'); final _logger = Logger('AudiobookPlayer');
// add a small offset so the display does not show the previous chapter for a split second
final 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
final doNotSeekBackIfLessThan = Duration(seconds: 5);
/// returns the sum of the duration of all the previous tracks before the [index] /// returns the sum of the duration of all the previous tracks before the [index]
Duration sumOfTracks(BookExpanded book, int? index) { Duration sumOfTracks(BookExpanded book, int? index) {
_logger.fine('Calculating sum of tracks for index: $index'); _logger.fine('Calculating sum of tracks for index: $index');
@ -31,31 +37,17 @@ Duration sumOfTracks(BookExpanded book, int? index) {
return total; return total;
} }
/// returns the [AudioTrack] to play based on the [position] in the [book]
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
_logger.fine('Getting track to play for position: $position');
final track = book.tracks.firstWhere(
(element) {
return element.startOffset <= position &&
(element.startOffset + element.duration) >= position;
},
orElse: () => book.tracks.last,
);
_logger.fine('Track to play for position: $position is $track');
return track;
}
/// will manage the audio player instance /// will manage the audio player instance
class AudiobookPlayer extends AudioPlayer { class AudiobookPlayer extends AudioPlayer {
// constructor which takes in the BookExpanded object // constructor which takes in the BookExpanded object
AudiobookPlayer(this.token, this.baseUrl) : super() { AudiobookPlayer(this.token, this.baseUrl) : super() {
// set the source of the player to the first track in the book // set the source of the player to the first track in the book
_logger.config('Setting up audiobook player'); _logger.config('Setting up audiobook player');
playerStateStream.listen((playerState) { // playerStateStream.listen((playerState) {
if (playerState.processingState == ProcessingState.completed) { // if (playerState.processingState == ProcessingState.completed) {
Future.microtask(seekToNext); // Future.microtask(seekToNext);
} // }
}); // });
} }
/// the [BookExpanded] being played /// the [BookExpanded] being played
@ -76,17 +68,16 @@ class AudiobookPlayer extends AudioPlayer {
final Uri baseUrl; final Uri baseUrl;
// the current index of the audio file in the [book] // the current index of the audio file in the [book]
int _currentIndex = 0; // int _currentIndex = 0;
// available audio tracks // available audio tracks
int? get availableTracks => _book?.tracks.length; int? get availableTracks => _book?.tracks.length;
List<Uri>? _downloadedUris;
/// sets the current [AudioTrack] as the source of the player /// sets the current [AudioTrack] as the source of the player
Future<void> setSourceAudiobook( Future<void> setSourceAudiobook(
BookExpanded? book, { BookExpanded? book, {
bool preload = true, bool preload = true,
// int? initialIndex, int? initialIndex,
Duration? initialPosition, Duration? initialPosition,
List<Uri>? downloadedUris, List<Uri>? downloadedUris,
Uri? artworkUri, Uri? artworkUri,
@ -94,7 +85,7 @@ class AudiobookPlayer extends AudioPlayer {
_logger.finer( _logger.finer(
'Initial position: $initialPosition, Downloaded URIs: $downloadedUris', 'Initial position: $initialPosition, Downloaded URIs: $downloadedUris',
); );
// final appSettings = loadOrCreateAppSettings(); final appSettings = loadOrCreateAppSettings();
if (book == null) { if (book == null) {
_book = null; _book = null;
_logger.info('Book is null, stopping player'); _logger.info('Book is null, stopping player');
@ -111,103 +102,52 @@ class AudiobookPlayer extends AudioPlayer {
await stop(); await stop();
_book = book; _book = book;
_downloadedUris = downloadedUris;
// some calculations to set the initial index and position // some calculations to set the initial index and position
// initialPosition is of the entire book not just the current track // initialPosition is of the entire book not just the current track
// hence first we need to calculate the current track which will be used to set the initial position // hence first we need to calculate the current track which will be used to set the initial position
// then we set the initial index to the current track index and position as the remaining duration from the position // then we set the initial index to the current track index and position as the remaining duration from the position
// after subtracting the duration of all the previous tracks // after subtracting the duration of all the previous tracks
// initialPosition ; // initialPosition ;
final trackToPlay = getTrackToPlay(book, initialPosition ?? Duration.zero); final trackToPlay =
_book!.findTrackAtTime(initialPosition ?? Duration.zero);
final initialIndex = book.tracks.indexOf(trackToPlay); final initialIndex = book.tracks.indexOf(trackToPlay);
final initialPositionInTrack = initialPosition != null final initialPositionInTrack = initialPosition != null
? initialPosition - trackToPlay.startOffset ? initialPosition - trackToPlay.startOffset
: null; : null;
await setAudioSourceTrack( _logger.finer('Setting audioSource');
initialIndex, final playlist = book.tracks.map((track) {
initialPosition: initialPositionInTrack, final retrievedUri =
); _getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
// _logger.finer('Setting audioSource'); // _logger.fine(
// await setAudioSource( // 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
// preload: preload, // );
// initialIndex: initialIndex, return AudioSource.uri(
// initialPosition: initialPositionInTrack, retrievedUri,
// ConcatenatingAudioSource(
// useLazyPreparation: true,
// children: book.tracks.map((track) {
// final retrievedUri = _getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
// _logger.fine(
// 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
// );
// return AudioSource.uri(
// retrievedUri,
// tag: MediaItem(
// // Specify a unique ID for each media item:
// id: book.libraryItemId + track.index.toString(),
// // Metadata to display in the notification:
// title: appSettings.notificationSettings.primaryTitle.formatNotificationTitle(book),
// album: appSettings.notificationSettings.secondaryTitle.formatNotificationTitle(book),
// artUri: artworkUri ??
// Uri.parse(
// '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
// ),
// ),
// );
// }).toList(),
// ),
// ).catchError((error) {
// _logger.shout('Error in setting audio source: $error');
// });
}
Future<void> setAudioSourceTrack(
int index, {
Duration? initialPosition,
}) async {
if (_book == null) {
return stop();
}
if (_currentIndex != 0 && index == _currentIndex) {
if (initialPosition != null) {
seek(initialPosition);
}
return;
}
_currentIndex = index;
AudioTrack track = _book!.tracks[index];
final appSettings = loadOrCreateAppSettings();
final playerSettings =
readFromBoxOrCreate(_book!.libraryItemId).playerSettings;
if (initialPosition == null || initialPosition <= Duration(seconds: 1)) {
initialPosition = playerSettings.skipChapterStart;
}
final retrievedUri =
_getUri(track, _downloadedUris, baseUrl: baseUrl, token: token);
await setAudioSource(
initialPosition: initialPosition,
ClippingAudioSource(
end: track.duration - playerSettings.skipChapterEnd,
child: AudioSource.uri(
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}', 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: Uri.parse( artUri: artworkUri ??
'$baseUrl/api/items/${book?.libraryItemId}/cover?token=$token&width=800', Uri.parse(
), '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
),
), ),
), );
); }).toList();
await setAudioSources(
playlist,
preload: preload,
initialIndex: initialIndex,
initialPosition: initialPositionInTrack,
).catchError((error) {
_logger.shout('Error in setting audio source: $error');
return null;
});
} }
/// toggles the player between play and pause /// toggles the player between play and pause
@ -223,145 +163,136 @@ class AudiobookPlayer extends AudioPlayer {
}; };
} }
// @override
// Future<void> seek(Duration? positionInBook, {int? index, bool b = true}) async {
// if (!b) {
// return super.seek(positionInBook, index: index);
// }
// if (_book == null) {
// _logger.warning('No book is set, not seeking');
// return;
// }
// if (positionInBook == null) {
// _logger.warning('Position given is null, not seeking');
// return;
// }
// final tracks = _book!.tracks;
// final trackToPlay = getTrackToPlay(_book!, positionInBook);
// final i = tracks.indexOf(trackToPlay);
// final positionInTrack = positionInBook - trackToPlay.startOffset;
// return super.seek(positionInTrack, index: i);
// }
/// need to override getDuration and getCurrentPosition to return according to the book instead of the current track /// need to override getDuration and getCurrentPosition to return according to the book instead of the current track
/// this is because the book can be a list of audio files and the player is only aware of the current track /// this is because the book can be a list of audio files and the player is only aware of the current track
/// so we need to calculate the duration and current position based on the book /// so we need to calculate the duration and current position based on the book
Future<void> seekInBook(Duration globalPosition) async {
Future<void> seekInBook(Duration? positionInBook, {int? index}) async {
if (_book == null) { if (_book == null) {
_logger.warning('No book is set, not seeking'); _logger.warning('No book is set, not seeking');
return; return;
} }
if (positionInBook == null) { //
_logger.warning('Position given is null, not seeking'); final track = _book!.findTrackAtTime(globalPosition);
return; final index = _book!.tracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack <= Duration.zero) {
positionInTrack = offset;
} }
final tracks = _book!.tracks; //
final trackToPlay = getTrackToPlay(_book!, positionInBook); if (index != currentIndex) {
final i = tracks.indexOf(trackToPlay); await seek(positionInTrack, index: index);
final positionInTrack = positionInBook - trackToPlay.startOffset; }
return setAudioSourceTrack(i, initialPosition: positionInTrack); await seek(positionInTrack);
// return super.seek(positionInTrack, index: i);
} }
// add a small offset so the display does not show the previous chapter for a split second //
final offset = Duration(milliseconds: 10); Future<void> skipToChapter(int chapterId, {Duration? position}) async {
if (_book == null) return;
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter final chapter = _book!.chapters.firstWhere(
final doNotSeekBackIfLessThan = Duration(seconds: 5); (ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
/// seek forward to the next chapter );
void seekForward() { if (position != null) {
seekInBook(currentChapter!.end + offset); print('章节开头: ${chapter.start}');
// final index = _book!.chapters.indexOf(currentChapter!); print('章节开头: ${chapter.start + position}');
// if (index < _book!.chapters.length - 1) { await seekInBook(chapter.start + position);
// super.seek(
// _book!.chapters[index + 1].start + offset,
// );
// } else {
// super.seek(currentChapter!.end);
// }
}
/// seek backward to the previous chapter or the start of the current chapter
void seekBackward() {
final currentPlayingChapterIndex = _book!.chapters.indexOf(currentChapter!);
if (position > doNotSeekBackIfLessThan || currentPlayingChapterIndex <= 0) {
seekInBook(currentChapter!.start + offset);
return; return;
} }
BookChapter chapterToSeekTo = await seekInBook(chapter.start + offset);
_book!.chapters[currentPlayingChapterIndex - 1];
seekInBook(chapterToSeekTo.start + offset);
// final currentPlayingChapterIndex = _book!.chapters.indexOf(currentChapter!);
// final chapterPosition = positionInBook - 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 = _book!.chapters[currentPlayingChapterIndex - 1];
// } else {
// chapterToSeekTo = currentChapter!;
// }
// super.seek(
// chapterToSeekTo.start + offset,
// );
} }
@override @override
Future<void> seekToNext() { Future<void> seekToNext() async {
if (_currentIndex >= availableTracks!) { if (_book == null) {
return super.seek(duration); // 退
return super.seekToNext();
}
final chapter = currentChapter;
if (chapter == null) {
// 退
return super.seekToNext();
}
final currentIndex = _book!.chapters.indexOf(chapter);
if (currentIndex < _book!.chapters.length - 1) {
//
final nextChapter = _book!.chapters[currentIndex + 1];
await skipToChapter(nextChapter.id);
} }
return setAudioSourceTrack(_currentIndex + 1);
} }
@override @override
Future<void> seekToPrevious() { Future<void> seekToPrevious() async {
if (_currentIndex == 0) { if (_book == null) {
return super.seek(Duration()); return super.seekToPrevious();
}
final chapter = currentChapter;
if (chapter == null) {
return super.seekToPrevious();
}
final currentIndex = _book!.chapters.indexOf(chapter);
if (currentIndex > 0) {
//
final prevChapter = _book!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id);
} else {
//
await seekInBook(Duration.zero);
} }
return setAudioSourceTrack(_currentIndex - 1);
} }
/// a convenience method to get position in the book instead of the current track position /// a convenience method to get position in the book instead of the current track position
Duration get positionInBook { Duration get positionInBook {
if (_book == null) { if (_book == null || currentIndex == null) {
return Duration.zero; return Duration.zero;
} }
return position + _book!.tracks[_currentIndex].startOffset; return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset; // return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
} }
/// a convenience method to get the buffered position in the book instead of the current track position /// a convenience method to get the buffered position in the book instead of the current track position
Duration get bufferedPositionInBook { Duration get bufferedPositionInBook {
if (_book == null) { if (_book == null || currentIndex == null) {
return Duration.zero; return Duration.zero;
} }
return bufferedPosition + _book!.tracks[_currentIndex].startOffset; return bufferedPosition + _book!.tracks[currentIndex!].startOffset;
// return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset; // return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset;
} }
//
Stream<Duration> get positionStreamInChapter {
return super.positionStream.map((position) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
final globalPosition =
position + _book!.tracks[currentIndex!].startOffset;
final chapter = _book!.findChapterAtTime(globalPosition);
return globalPosition - chapter.start;
});
}
/// streams to override to suit the book instead of the current track /// streams to override to suit the book instead of the current track
// - positionStream // - positionStream
// - bufferedPositionStream // - bufferedPositionStream
Stream<Duration> get positionStreamInBook { Stream<Duration> get positionStreamInBook {
// return the positionInBook stream // return the positionInBook stream
return super.positionStream.map((position) { return super.positionStream.map((position) {
if (_book == null) { if (_book == null || currentIndex == null) {
return Duration.zero; return Duration.zero;
} }
return position + _book!.tracks[_currentIndex].startOffset; return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset; // return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
}); });
} }
Stream<Duration> get bufferedPositionStreamInBook { Stream<Duration> get bufferedPositionStreamInBook {
return super.bufferedPositionStream.map((position) { return super.bufferedPositionStream.map((position) {
if (_book == null) { if (_book == null || currentIndex == null) {
return Duration.zero; return Duration.zero;
} }
return position + _book!.tracks[_currentIndex].startOffset; return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset; // return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
}); });
} }
@ -375,10 +306,10 @@ class AudiobookPlayer extends AudioPlayer {
); );
// now we need to map the position to the book instead of the current track // now we need to map the position to the book instead of the current track
return superPositionStream.map((position) { return superPositionStream.map((position) {
if (_book == null) { if (_book == null || currentIndex == null) {
return Duration.zero; return Duration.zero;
} }
return position + _book!.tracks[_currentIndex].startOffset; return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset; // return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
}); });
} }
@ -388,17 +319,7 @@ class AudiobookPlayer extends AudioPlayer {
if (_book == null) { if (_book == null) {
return null; return null;
} }
// if the list is empty, return null return _book!.findChapterAtTime(positionInBook);
if (_book!.chapters.isEmpty) {
return null;
}
return _book!.chapters.firstWhere(
(element) {
return element.start <= positionInBook &&
element.end >= positionInBook + offset;
},
orElse: () => _book!.chapters.first,
);
} }
} }
@ -457,3 +378,28 @@ extension NotificationTitleUtils on NotificationTitleType {
} }
} }
} }
extension BookExpandedExtension on BookExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position + offset;
},
orElse: () => chapters.first,
);
}
AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => tracks.first,
);
}
Duration getTrackStartOffset(int index) {
return tracks[index].startOffset;
}
}

View file

@ -0,0 +1,272 @@
// my_audio_handler.dart
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
// add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10);
class HookAudioHandler extends BaseAudioHandler {
final AudioPlayer _player = AudioPlayer();
final List<AudioSource> _playlist = [];
final Ref ref;
BookExpanded? _book;
/// the authentication token to access the [AudioTrack.contentUrl]
final String token;
/// the base url for the audio files
final Uri baseUrl;
HookAudioHandler(this.ref, {required this.token, required this.baseUrl}) {
_setupAudioPlayer();
}
void _setupAudioPlayer() {
_player.setAudioSources(_playlist);
// //
// _player.positionStream.listen((position) {
// // _updateGlobalPosition(position);
// });
// //
// _player.currentIndexStream.listen((index) {
// if (index != null) {
// _onTrackChanged(index);
// }
// });
//
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
}
//
Future<void> setSourceAudiobook(
BookExpanded audiobook, {
Duration? initialPosition,
List<Uri>? downloadedUris,
}) async {
_book = audiobook;
//
_playlist.clear();
//
for (final track in audiobook.tracks) {
final audioSource = ProgressiveAudioSource(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
tag: MediaItem(
id: '${audiobook.libraryItemId}${track.index}',
title: track.title,
duration: track.duration,
),
);
_playlist.add(audioSource);
}
//
final mediaItems = audiobook.tracks
.map(
(track) => MediaItem(
id: '${audiobook.libraryItemId}${track.index}',
title: track.title,
duration: track.duration,
),
)
.toList();
queue.add(mediaItems);
//
if (initialPosition != null) {
await seekToPosition(initialPosition);
}
}
// //
// void _onTrackChanged(int trackIndex) {
// if (_book == null) return;
// //
// // print('切换到音轨: ${_book!.tracks[trackIndex].title}');
// }
//
Future<void> skipToChapter(int chapterId) async {
if (_book == null) return;
final chapter = _book!.chapters.firstWhere(
(ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
);
await seekToPosition(chapter.start + offset);
}
Duration get positionInBook {
if (_book != null && _player.currentIndex != null) {
return _book!.tracks[_player.currentIndex!].startOffset +
_player.position;
}
return Duration.zero;
}
//
AudioTrack? get currentTrack {
if (_book == null) {
return null;
}
return _book!.findTrackAtTime(positionInBook);
}
//
BookChapter? get currentChapter {
if (_book == null) {
return null;
}
return _book!.findChapterAtTime(positionInBook);
}
//
@override
Future<void> play() => _player.play();
@override
Future<void> pause() => _player.pause();
// /
@override
Future<void> skipToNext() async {
if (_book == null) {
// 退
return _player.seekToNext();
}
final chapter = currentChapter;
if (chapter == null) {
// 退
return _player.seekToNext();
}
final currentIndex = _book!.chapters.indexOf(chapter);
if (currentIndex < _book!.chapters.length - 1) {
//
final nextChapter = _book!.chapters[currentIndex + 1];
await skipToChapter(nextChapter.id);
}
}
@override
Future<void> skipToPrevious() async {
if (_book == null) {
return _player.seekToPrevious();
}
final chapter = currentChapter;
if (chapter == null) {
return _player.seekToPrevious();
}
final currentIndex = _book!.chapters.indexOf(chapter);
if (currentIndex > 0) {
//
final prevChapter = _book!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id);
} else {
//
await seekToPosition(Duration.zero);
}
}
@override
Future<void> seek(Duration position) async {
// position 使
//
final track = currentTrack;
Duration startOffset = Duration.zero;
if (track != null) {
startOffset = track.startOffset;
}
await seekToPosition(startOffset + position);
}
//
Future<void> seekToPosition(Duration globalPosition) async {
if (_book == null) return;
//
final track = _book!.findTrackAtTime(globalPosition);
final index = _book!.tracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack <= Duration.zero) {
positionInTrack = offset;
}
//
await _player.seek(positionInTrack, index: index);
}
PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState] ??
AudioProcessingState.idle,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
);
}
}
Uri _getUri(
AudioTrack track,
List<Uri>? downloadedUris, {
required Uri baseUrl,
required String token,
}) {
// check if the track is in the downloadedUris
final uri = downloadedUris?.firstWhereOrNull(
(element) {
return element.pathSegments.last == track.metadata?.filename;
},
);
return uri ??
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
}
extension BookExpandedExtension on BookExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position + offset;
},
orElse: () => chapters.first,
);
}
AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => tracks.first,
);
}
Duration getTrackStartOffset(int index) {
return tracks[index].startOffset;
}
}

View file

@ -9,7 +9,7 @@ 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();
// 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;

View file

@ -1,3 +1,4 @@
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:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
@ -51,3 +52,12 @@ class AudiobookPlayer extends _$AudiobookPlayer {
ref.notifyListeners(); ref.notifyListeners();
} }
} }
@riverpod
bool isPlayerPlaying(
Ref ref,
) {
final player = ref.watch(audiobookPlayerProvider);
print("playing: ${player.playing}");
return player.playing;
}

View file

@ -6,6 +6,23 @@ part of 'audiobook_player.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$isPlayerPlayingHash() => r'b81fa9cfb51c88c8d9e8f5c1f4f6a12d9e5a0cc1';
/// See also [isPlayerPlaying].
@ProviderFor(isPlayerPlaying)
final isPlayerPlayingProvider = AutoDisposeProvider<bool>.internal(
isPlayerPlaying,
name: r'isPlayerPlayingProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$isPlayerPlayingHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef IsPlayerPlayingRef = AutoDisposeProviderRef<bool>;
String _$simpleAudiobookPlayerHash() => String _$simpleAudiobookPlayerHash() =>
r'5e94bbff4314adceb5affa704fc4d079d4016afa'; r'5e94bbff4314adceb5affa704fc4d079d4016afa';

View file

@ -4,7 +4,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:miniplayer/miniplayer.dart'; // import 'package:miniplayer/miniplayer.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/audiobook_player.dart';
@ -60,7 +60,7 @@ double playerHeight(
return playerExpandProgress.value; return playerExpandProgress.value;
} }
final audioBookMiniplayerController = MiniplayerController(); // final audioBookMiniplayerController = MiniplayerController();
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
bool isPlayerActive( bool isPlayerActive(

View file

@ -0,0 +1 @@

View file

@ -1,271 +1,195 @@
import 'dart:math'; // import 'dart:math';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; // import 'package:flutter/material.dart';
import 'package:flutter/material.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:just_audio/just_audio.dart';
import 'package:just_audio/just_audio.dart'; // import 'package:miniplayer/miniplayer.dart';
import 'package:miniplayer/miniplayer.dart'; // import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/image_provider.dart'; // import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/api/library_item_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/currently_playing_provider.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart'; // import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/features/player/providers/player_form.dart'; // import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/app_settings_provider.dart'; // import 'package:vaani/shared/extensions/inverse_lerp.dart';
import 'package:vaani/shared/extensions/inverse_lerp.dart'; // import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; // import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
import 'player_when_expanded.dart'; // import 'player_when_expanded.dart';
import 'player_when_minimized.dart'; // import 'player_when_minimized.dart';
const playerMaxHeightPercentOfScreen = 0.8; // const playerMaxHeightPercentOfScreen = 0.8;
class AudiobookPlayer extends HookConsumerWidget { // class AudiobookPlayer extends HookConsumerWidget {
const AudiobookPlayer({super.key}); // const AudiobookPlayer({super.key});
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider); // final appSettings = ref.watch(appSettingsProvider);
final currentBook = ref.watch(currentlyPlayingBookProvider); // final currentBook = ref.watch(currentlyPlayingBookProvider);
if (currentBook == null) { // if (currentBook == null) {
return const SizedBox.shrink(); // return const SizedBox.shrink();
} // }
final itemBeingPlayed = // final itemBeingPlayed =
ref.watch(libraryItemProvider(currentBook.libraryItemId)); // ref.watch(libraryItemProvider(currentBook.libraryItemId));
final player = ref.watch(audiobookPlayerProvider); // final player = ref.watch(audiobookPlayerProvider);
final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null // final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null
? ref.watch( // ? ref.watch(
coverImageProvider(itemBeingPlayed.valueOrNull!.id), // coverImageProvider(itemBeingPlayed.valueOrNull!.id),
) // )
: null; // : null;
final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null // final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null
? Image.memory( // ? Image.memory(
imageOfItemBeingPlayed!.valueOrNull!, // imageOfItemBeingPlayed!.valueOrNull!,
fit: BoxFit.cover, // fit: BoxFit.cover,
) // )
: const BookCoverSkeleton(); // : const BookCoverSkeleton();
final playPauseController = useAnimationController( // final playPauseController = useAnimationController(
duration: const Duration(milliseconds: 200), // duration: const Duration(milliseconds: 200),
initialValue: 1, // initialValue: 1,
); // );
// add controller to the player state listener // // add controller to the player state listener
player.playerStateStream.listen((state) { // player.playerStateStream.listen((state) {
if (state.playing) { // if (state.playing) {
playPauseController.forward(); // playPauseController.forward();
} else { // } else {
playPauseController.reverse(); // playPauseController.reverse();
} // }
}); // });
// theme from image // // theme from image
final imageTheme = ref.watch( // final imageTheme = ref.watch(
themeOfLibraryItemProvider( // themeOfLibraryItemProvider(
itemBeingPlayed.valueOrNull?.id, // itemBeingPlayed.valueOrNull?.id,
brightness: Theme.of(context).brightness, // brightness: Theme.of(context).brightness,
highContrast: appSettings.themeSettings.highContrast || // highContrast: appSettings.themeSettings.highContrast ||
MediaQuery.of(context).highContrast, // MediaQuery.of(context).highContrast,
), // ),
); // );
// 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 maxImgSize = min(playerMaxHeight * 0.5, availWidth * 0.9); // final maxImgSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
final preferredVolume = appSettings.playerSettings.preferredDefaultVolume; // final preferredVolume = appSettings.playerSettings.preferredDefaultVolume;
return Theme( // return Theme(
data: ThemeData( // data: ThemeData(
colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme, // colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
), // ),
child: Miniplayer( // child: Miniplayer(
valueNotifier: ref.watch(playerExpandProgressNotifierProvider), // valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
onDragDown: (percentage) async { // onDragDown: (percentage) async {
// preferred volume // // preferred volume
// set volume to 0 when dragging down // // set volume to 0 when dragging down
await player // await player
.setVolume(preferredVolume * (1 - percentage.clamp(0, .75))); // .setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
}, // },
minHeight: playerMinHeight, // minHeight: playerMinHeight,
// subtract the height of notches and other system UI // // subtract the height of notches and other system UI
maxHeight: playerMaxHeight, // maxHeight: playerMaxHeight,
controller: audioBookMiniplayerController, // controller: audioBookMiniplayerController,
elevation: 4, // elevation: 4,
// duration: Duration(seconds: 3), // // duration: Duration(seconds: 3),
onDismissed: () { // onDismissed: () {
// add a delay before closing the player // // add a delay before closing the player
// to allow the user to see the player closing // // to allow the user to see the player closing
Future.delayed(const Duration(milliseconds: 300), () { // Future.delayed(const Duration(milliseconds: 300), () {
player.setSourceAudiobook(null); // player.setSourceAudiobook(null);
}); // });
}, // },
curve: Curves.linear, // curve: Curves.linear,
builder: (height, percentage) { // builder: (height, percentage) {
// at what point should the player switch from miniplayer to expanded player // // at what point should the player switch from miniplayer to expanded player
// also at this point the image should be at its max size and in the center of the player // // also at this point the image should be at its max size and in the center of the player
final miniplayerPercentageDeclaration = // final miniplayerPercentageDeclaration =
(maxImgSize - playerMinHeight) / // (maxImgSize - playerMinHeight) /
(playerMaxHeight - playerMinHeight); // (playerMaxHeight - playerMinHeight);
final bool isFormMiniplayer = // final bool isFormMiniplayer =
percentage < miniplayerPercentageDeclaration; // percentage < miniplayerPercentageDeclaration;
if (!isFormMiniplayer) { // if (!isFormMiniplayer) {
// this calculation needs a refactor // // this calculation needs a refactor
var percentageExpandedPlayer = percentage // var percentageExpandedPlayer = percentage
.inverseLerp( // .inverseLerp(
miniplayerPercentageDeclaration, // miniplayerPercentageDeclaration,
1, // 1,
) // )
.clamp(0.0, 1.0); // .clamp(0.0, 1.0);
return PlayerWhenExpanded( // return PlayerWhenExpanded(
imageSize: maxImgSize, // imageSize: maxImgSize,
img: imgWidget, // img: imgWidget,
percentageExpandedPlayer: percentageExpandedPlayer, // percentageExpandedPlayer: percentageExpandedPlayer,
playPauseController: playPauseController, // playPauseController: playPauseController,
); // );
} // }
//Miniplayer // //Miniplayer
final percentageMiniplayer = percentage.inverseLerp( // final percentageMiniplayer = percentage.inverseLerp(
0, // 0,
miniplayerPercentageDeclaration, // miniplayerPercentageDeclaration,
); // );
return PlayerWhenMinimized( // return PlayerWhenMinimized(
maxImgSize: maxImgSize, // maxImgSize: maxImgSize,
availWidth: availWidth, // availWidth: availWidth,
imgWidget: imgWidget, // imgWidget: imgWidget,
playPauseController: playPauseController, // playPauseController: playPauseController,
percentageMiniplayer: percentageMiniplayer, // percentageMiniplayer: percentageMiniplayer,
); // );
}, // },
), // ),
); // );
} // }
} // }
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { // class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({ // const AudiobookPlayerPlayPauseButton({
super.key, // super.key,
required this.playPauseController, // required this.playPauseController,
this.iconSize = 48.0, // this.iconSize = 48.0,
}); // });
final double iconSize; // final double iconSize;
final AnimationController playPauseController; // final AnimationController playPauseController;
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); // final player = ref.watch(audiobookPlayerProvider);
return switch (player.processingState) { // return switch (player.processingState) {
ProcessingState.loading || ProcessingState.buffering => const Padding( // ProcessingState.loading || ProcessingState.buffering => const Padding(
padding: EdgeInsets.all(8.0), // padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(), // child: CircularProgressIndicator(),
), // ),
ProcessingState.completed => IconButton( // ProcessingState.completed => IconButton(
onPressed: () async { // onPressed: () async {
await player.seekInBook(const Duration(seconds: 0)); // await player.seekInBook(const Duration(seconds: 0));
await player.play(); // await player.play();
}, // },
icon: const Icon( // icon: const Icon(
Icons.replay, // Icons.replay,
), // ),
), // ),
ProcessingState.ready => IconButton( // ProcessingState.ready => IconButton(
onPressed: () async { // onPressed: () async {
await player.togglePlayPause(); // await player.togglePlayPause();
}, // },
iconSize: iconSize, // iconSize: iconSize,
icon: AnimatedIcon( // icon: AnimatedIcon(
icon: AnimatedIcons.play_pause, // icon: AnimatedIcons.play_pause,
progress: playPauseController, // progress: playPauseController,
), // ),
), // ),
ProcessingState.idle => const SizedBox.shrink(), // ProcessingState.idle => const SizedBox.shrink(),
}; // };
} // }
} // }
class AudiobookChapterProgressBar extends HookConsumerWidget { // // ! TODO remove onTap
const AudiobookChapterProgressBar({ // void onTap() {}
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider);
final position = useStream(
player.positionStreamInBook,
initialData: const Duration(seconds: 0),
);
final buffered = useStream(
player.bufferedPositionStreamInBook,
initialData: const Duration(seconds: 0),
);
// now find the chapter that corresponds to the current time
// and calculate the progress of the current chapter
final currentChapterProgress = currentChapter == null
? null
: (player.positionInBook - currentChapter.start);
final currentChapterBuffered = currentChapter == null
? null
: (player.bufferedPositionInBook - currentChapter.start);
return ProgressBar(
progress:
currentChapterProgress ?? position.data ?? const Duration(seconds: 0),
total: currentChapter == null
? player.book?.duration ?? const Duration(seconds: 0)
: currentChapter.end - currentChapter.start,
// ! TODO add onSeek
onSeek: (duration) {
player.seekInBook(
duration + (currentChapter?.start ?? const Duration(seconds: 0)),
);
// player.seek(duration);
},
thumbRadius: 8,
buffered:
currentChapterBuffered ?? buffered.data ?? const Duration(seconds: 0),
bufferedBarColor: Theme.of(context).colorScheme.secondary,
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}
class AudiobookProgressBar extends HookConsumerWidget {
const AudiobookProgressBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final position = useStream(
player.slowPositionStreamInBook,
initialData: const Duration(seconds: 0),
);
return ProgressBar(
progress: position.data ?? const Duration(seconds: 0),
total: player.book?.duration ?? const Duration(seconds: 0),
thumbRadius: 8,
bufferedBarColor: Theme.of(context).colorScheme.secondary,
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}
// ! TODO remove onTap
void onTap() {}

View file

@ -0,0 +1,227 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
import 'package:vaani/features/skip_start_end/player_skip_chapter_start_end.dart';
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/shared/widgets/not_implemented.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
import 'widgets/audiobook_player_seek_button.dart';
import 'widgets/audiobook_player_seek_chapter_button.dart';
import 'widgets/chapter_selection_button.dart';
import 'widgets/player_speed_adjust_button.dart';
var pendingPlayerModals = 0;
class PlayerExpanded extends HookConsumerWidget {
const PlayerExpanded({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
/// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer]
/// however, some properties need to start later than 0% and end before 100%
final currentBook = ref.watch(currentlyPlayingBookProvider);
if (currentBook == null) {
return const SizedBox.shrink();
}
final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
// max height of the player is the height of the screen
final playerMaxHeight = MediaQuery.of(context).size.height;
final availWidth = MediaQuery.of(context).size.width;
// the image width when the player is expanded
final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
final itemBeingPlayed =
ref.watch(libraryItemProvider(currentBook.libraryItemId));
final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null
? ref.watch(
coverImageProvider(itemBeingPlayed.valueOrNull!.id),
)
: null;
final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null
? Image.memory(
imageOfItemBeingPlayed!.valueOrNull!,
fit: BoxFit.cover,
)
: const BookCoverSkeleton();
return Scaffold(
appBar: AppBar(
leading: IconButton(
iconSize: 30,
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => context.pop(),
),
actions: [
IconButton(
icon: const Icon(Icons.cast),
onPressed: () {
showNotImplementedToast(context);
},
),
],
),
body: Column(
children: [
// sized box for system status bar; not needed as not full screen
SizedBox(
height: MediaQuery.of(context).padding.top,
),
// the image
Padding(
padding: EdgeInsets.only(top: AppElementSizes.paddingLarge),
child: Align(
alignment: Alignment.center,
// add a shadow to the image elevation hovering effect
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.1),
blurRadius: 32,
spreadRadius: 8,
),
],
),
child: SizedBox(
height: imageSize,
child: InkWell(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular,
),
child: imgWidget,
),
),
),
),
),
),
// the chapter title
Expanded(
child: Padding(
padding: EdgeInsets.only(top: AppElementSizes.paddingRegular),
child: currentChapter == null
? const SizedBox()
: Text(
currentChapter.title,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// the book name and author
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular),
child: Text(
[
currentBookMetadata?.title ?? '',
currentBookMetadata?.authorName ?? '',
].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// the progress bar
Expanded(
child: SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
left: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular,
),
child: const AudiobookChapterProgressBar(),
),
),
),
Expanded(
child: SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
left: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular,
),
child: const AudiobookProgressBar(),
),
),
),
// the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
Expanded(
flex: 2,
child: SizedBox(
width: imageSize,
height: AppElementSizes.iconSizeRegular,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// previous chapter
const AudiobookPlayerSeekChapterButton(isForward: false),
// buttonSkipBackwards
const AudiobookPlayerSeekButton(isForward: false),
AudiobookPlayerPlayPauseButton(),
// buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true),
// next chapter
const AudiobookPlayerSeekChapterButton(isForward: true),
],
),
),
),
// speed control, sleep timer, chapter list, and settings
Expanded(
child: SizedBox(
width: imageSize,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// speed control
const PlayerSpeedAdjustButton(),
const Spacer(),
// sleep timer
const SleepTimerButton(),
const Spacer(),
// chapter list
const ChapterSelectionButton(),
const Spacer(),
//
SkipChapterStartEndButton(),
],
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
/// The height of the player when it is minimized
const double playerMinimizedHeight = 70;
class PlayerMinimized extends HookConsumerWidget {
const PlayerMinimized({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentBook = ref.watch(currentlyPlayingBookProvider);
if (currentBook == null) {
return const SizedBox.shrink();
}
final itemBeingPlayed =
ref.watch(libraryItemProvider(currentBook.libraryItemId));
final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null
? ref.watch(
coverImageProvider(itemBeingPlayed.valueOrNull!.id),
)
: null;
final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null
? Image.memory(
imageOfItemBeingPlayed!.valueOrNull!,
fit: BoxFit.cover,
)
: const BookCoverSkeleton();
final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider);
return PlayerMinimizedFramework(
children: [
// image
Padding(
padding: EdgeInsets.all(AppElementSizes.paddingSmall),
child: InkWell(
onTap: () {
// navigate to item page
context.pushNamed(
Routes.libraryItem.name,
pathParameters: {
Routes.libraryItem.pathParamName!: currentBook.libraryItemId,
},
);
},
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: playerMinimizedHeight,
),
child: imgWidget,
),
),
),
// author and title of the book
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: AppElementSizes.paddingRegular,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// AutoScrollText(
Text(
'${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}',
maxLines: 1, overflow: TextOverflow.ellipsis,
// velocity:
// const Velocity(pixelsPerSecond: Offset(16, 0)),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
bookMetaExpanded?.authorName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
),
],
),
),
),
// rewind button
Padding(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
icon: const Icon(
Icons.replay_30,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {},
),
),
// play/pause button
Padding(
padding: const EdgeInsets.only(right: 8),
child: AudiobookPlayerPlayPauseButton(),
),
],
);
}
}
class PlayerMinimizedFramework extends HookConsumerWidget {
final List<Widget> children;
const PlayerMinimizedFramework({super.key, required this.children});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final progress =
useStream(player.positionStream, initialData: Duration.zero);
return GestureDetector(
onTap: () => context.pushNamed(Routes.player.name),
child: Container(
height: playerMinimizedHeight,
color: Theme.of(context).colorScheme.surface,
child: Stack(
alignment: Alignment.topCenter,
children: [
Row(
children: children,
),
SizedBox(
height: AppElementSizes.barHeight,
child: LinearProgressIndicator(
// value: (progress.data ?? Duration.zero).inSeconds /
// player.book!.duration.inSeconds,
value: (progress.data ?? Duration.zero).inSeconds /
(player.duration?.inSeconds ?? 1),
color: Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
),
],
),
),
);
}
}

View file

@ -1,292 +1,293 @@
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:miniplayer/miniplayer.dart'; // import 'package:miniplayer/miniplayer.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/currently_playing_provider.dart';
import 'package:vaani/features/player/providers/player_form.dart'; // import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/features/player/view/audiobook_player.dart'; // import 'package:vaani/features/player/view/audiobook_player.dart';
import 'package:vaani/features/skip_start_end/player_skip_chapter_start_end.dart'; // import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; // import 'package:vaani/features/skip_start_end/player_skip_chapter_start_end.dart';
import 'package:vaani/shared/extensions/inverse_lerp.dart'; // import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/shared/widgets/not_implemented.dart'; // import 'package:vaani/shared/extensions/inverse_lerp.dart';
// import 'package:vaani/shared/widgets/not_implemented.dart';
import 'widgets/audiobook_player_seek_button.dart'; // import 'widgets/audiobook_player_seek_button.dart';
import 'widgets/audiobook_player_seek_chapter_button.dart'; // import 'widgets/audiobook_player_seek_chapter_button.dart';
import 'widgets/chapter_selection_button.dart'; // import 'widgets/chapter_selection_button.dart';
import 'widgets/player_speed_adjust_button.dart'; // import 'widgets/player_speed_adjust_button.dart';
var pendingPlayerModals = 0; // var pendingPlayerModals = 0;
class PlayerWhenExpanded extends HookConsumerWidget { // class PlayerWhenExpanded extends HookConsumerWidget {
const PlayerWhenExpanded({ // const PlayerWhenExpanded({
super.key, // super.key,
required this.imageSize, // required this.imageSize,
required this.img, // required this.img,
required this.percentageExpandedPlayer, // required this.percentageExpandedPlayer,
required this.playPauseController, // required this.playPauseController,
}); // });
/// padding values control the position of the image // /// padding values control the position of the image
final double imageSize; // final double imageSize;
final Widget img; // final Widget img;
final double percentageExpandedPlayer; // final double percentageExpandedPlayer;
final AnimationController playPauseController; // final AnimationController playPauseController;
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
/// 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%
const lateStart = 0.4; // const lateStart = 0.4;
const earlyEnd = 1; // const earlyEnd = 1;
final earlyPercentage = percentageExpandedPlayer // final earlyPercentage = percentageExpandedPlayer
.inverseLerp( // .inverseLerp(
lateStart, // lateStart,
earlyEnd, // earlyEnd,
) // )
.clamp(0.0, 1.0); // .clamp(0.0, 1.0);
final currentChapter = ref.watch(currentPlayingChapterProvider); // final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider); // final currentBookMetadata = ref.watch(currentBookMetadataProvider);
return Column( // return Column(
children: [ // children: [
// sized box for system status bar; not needed as not full screen // // sized box for system status bar; not needed as not full screen
SizedBox( // SizedBox(
height: MediaQuery.of(context).padding.top * earlyPercentage, // height: MediaQuery.of(context).padding.top * earlyPercentage,
), // ),
// a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button // // a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button
ConstrainedBox( // ConstrainedBox(
constraints: BoxConstraints( // constraints: BoxConstraints(
maxHeight: 100 * earlyPercentage, // maxHeight: 100 * earlyPercentage,
), // ),
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: Padding( // child: Padding(
padding: EdgeInsets.only(top: 8.0 * earlyPercentage), // padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
child: Row( // child: Row(
crossAxisAlignment: CrossAxisAlignment.center, // crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max, // mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ // children: [
// the down arrow // // the down arrow
IconButton( // IconButton(
iconSize: 30, // iconSize: 30,
icon: const Icon(Icons.keyboard_arrow_down), // icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () { // onPressed: () {
// minimize the player // // minimize the player
audioBookMiniplayerController.animateToHeight( // audioBookMiniplayerController.animateToHeight(
state: PanelState.MIN, // state: PanelState.MIN,
); // );
}, // },
), // ),
// the cast button // // the cast button
IconButton( // IconButton(
icon: const Icon(Icons.cast), // icon: const Icon(Icons.cast),
onPressed: () { // onPressed: () {
showNotImplementedToast(context); // showNotImplementedToast(context);
}, // },
), // ),
], // ],
), // ),
), // ),
), // ),
), // ),
// the image // // the image
Padding( // Padding(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
top: AppElementSizes.paddingLarge * earlyPercentage, // top: AppElementSizes.paddingLarge * earlyPercentage,
), // ),
child: Align( // child: Align(
alignment: Alignment.center, // alignment: Alignment.center,
// add a shadow to the image elevation hovering effect // // add a shadow to the image elevation hovering effect
child: Container( // child: Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Theme.of(context) // color: Theme.of(context)
.colorScheme // .colorScheme
.primary // .primary
.withValues(alpha: 0.1), // .withValues(alpha: 0.1),
blurRadius: 32 * earlyPercentage, // blurRadius: 32 * earlyPercentage,
spreadRadius: 8 * earlyPercentage, // spreadRadius: 8 * earlyPercentage,
// offset: Offset(0, 16 * earlyPercentage), // // offset: Offset(0, 16 * earlyPercentage),
), // ),
], // ],
), // ),
child: SizedBox( // child: SizedBox(
height: imageSize, // height: imageSize,
child: InkWell( // child: InkWell(
onTap: () {}, // onTap: () {},
child: ClipRRect( // child: ClipRRect(
borderRadius: BorderRadius.circular( // borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular * earlyPercentage, // AppElementSizes.borderRadiusRegular * earlyPercentage,
), // ),
child: img, // child: img,
), // ),
), // ),
), // ),
), // ),
), // ),
), // ),
// the chapter title // // the chapter title
Expanded( // Expanded(
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: Padding( // child: Padding(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
top: AppElementSizes.paddingRegular * earlyPercentage, // top: AppElementSizes.paddingRegular * earlyPercentage,
// horizontal: 16.0, // // horizontal: 16.0,
), // ),
// child: SizedBox( // // child: SizedBox(
// same as the image width // // same as the image width
// width: imageSize, // // width: imageSize,
child: currentChapter == null // child: currentChapter == null
? const SizedBox() // ? const SizedBox()
: Text( // : Text(
currentChapter.title, // currentChapter.title,
style: Theme.of(context).textTheme.titleLarge, // style: Theme.of(context).textTheme.titleLarge,
maxLines: 1, // maxLines: 1,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
// ), // // ),
), // ),
), // ),
), // ),
// the book name and author // // the book name and author
Expanded( // Expanded(
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: Padding( // child: Padding(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
bottom: AppElementSizes.paddingRegular * earlyPercentage, // bottom: AppElementSizes.paddingRegular * earlyPercentage,
// horizontal: 16.0, // // horizontal: 16.0,
), // ),
// child: SizedBox( // // child: SizedBox(
// same as the image width // // same as the image width
// width: imageSize, // // width: imageSize,
child: Text( // child: Text(
[ // [
currentBookMetadata?.title ?? '', // currentBookMetadata?.title ?? '',
currentBookMetadata?.authorName ?? '', // currentBookMetadata?.authorName ?? '',
].join(' - '), // ].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith( // style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context) // color: Theme.of(context)
.colorScheme // .colorScheme
.onSurface // .onSurface
.withValues(alpha: 0.7), // .withValues(alpha: 0.7),
), // ),
maxLines: 1, // maxLines: 1,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
// ), // // ),
), // ),
), // ),
), // ),
// the progress bar // // the progress bar
Expanded( // Expanded(
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: SizedBox( // child: SizedBox(
width: imageSize, // width: imageSize,
child: Padding( // child: Padding(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
// top: AppElementSizes.paddingRegular * earlyPercentage, // // top: AppElementSizes.paddingRegular * earlyPercentage,
left: AppElementSizes.paddingRegular * earlyPercentage, // left: AppElementSizes.paddingRegular * earlyPercentage,
right: AppElementSizes.paddingRegular * earlyPercentage, // right: AppElementSizes.paddingRegular * earlyPercentage,
), // ),
child: const AudiobookChapterProgressBar(), // child: const AudiobookChapterProgressBar(),
), // ),
), // ),
), // ),
), // ),
Expanded( // Expanded(
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: SizedBox( // child: SizedBox(
width: imageSize, // width: imageSize,
child: Padding( // child: Padding(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
// top: AppElementSizes.paddingRegular * earlyPercentage, // // top: AppElementSizes.paddingRegular * earlyPercentage,
left: AppElementSizes.paddingRegular * earlyPercentage, // left: AppElementSizes.paddingRegular * earlyPercentage,
right: AppElementSizes.paddingRegular * earlyPercentage, // right: AppElementSizes.paddingRegular * earlyPercentage,
), // ),
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(
flex: 2, // flex: 2,
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: SizedBox( // child: SizedBox(
width: imageSize, // width: imageSize,
height: AppElementSizes.iconSizeRegular, // height: AppElementSizes.iconSizeRegular,
child: Row( // child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ // children: [
// previous chapter // // previous chapter
const AudiobookPlayerSeekChapterButton(isForward: false), // const AudiobookPlayerSeekChapterButton(isForward: false),
// buttonSkipBackwards // // buttonSkipBackwards
const AudiobookPlayerSeekButton(isForward: false), // const AudiobookPlayerSeekButton(isForward: false),
AudiobookPlayerPlayPauseButton( // AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController, // playPauseController: playPauseController,
), // ),
// buttonSkipForwards // // buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true), // const AudiobookPlayerSeekButton(isForward: true),
// next chapter // // next chapter
const AudiobookPlayerSeekChapterButton(isForward: true), // const AudiobookPlayerSeekChapterButton(isForward: true),
], // ],
), // ),
), // ),
), // ),
), // ),
// speed control, sleep timer, chapter list, and settings // // speed control, sleep timer, chapter list, and settings
Expanded( // Expanded(
child: Opacity( // child: Opacity(
opacity: earlyPercentage, // opacity: earlyPercentage,
child: SizedBox( // child: SizedBox(
// padding: EdgeInsets.only( // // padding: EdgeInsets.only(
// bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage, // // bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage,
// ), // // ),
width: imageSize, // width: imageSize,
child: Row( // child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ // children: [
// speed control // // speed control
const PlayerSpeedAdjustButton(), // const PlayerSpeedAdjustButton(),
const Spacer(), // const Spacer(),
// sleep timer // // sleep timer
const SleepTimerButton(), // const SleepTimerButton(),
const Spacer(), // const Spacer(),
// chapter list // // chapter list
const ChapterSelectionButton(), // const ChapterSelectionButton(),
const Spacer(), // const Spacer(),
// // //
SkipChapterStartEndButton(), // SkipChapterStartEndButton(),
// settings // // settings
// IconButton( // // IconButton(
// icon: const Icon(Icons.more_horiz), // // icon: const Icon(Icons.more_horiz),
// onPressed: () { // // onPressed: () {
// // show toast // // // show toast
// showNotImplementedToast(context); // // showNotImplementedToast(context);
// }, // // },
// ), // // ),
], // ],
), // ),
), // ),
), // ),
), // ),
], // ],
); // );
} // }
} // }

View file

@ -1,155 +1,155 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; // import 'package:flutter_hooks/flutter_hooks.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/constants/sizes.dart'; // import 'package:vaani/constants/sizes.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/currently_playing_provider.dart'; // import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/audiobook_player.dart'; // import 'package:vaani/features/player/view/audiobook_player.dart';
import 'package:vaani/router/router.dart'; // import 'package:vaani/router/router.dart';
class PlayerWhenMinimized extends HookConsumerWidget { // class PlayerWhenMinimized extends HookConsumerWidget {
const PlayerWhenMinimized({ // const PlayerWhenMinimized({
super.key, // super.key,
required this.availWidth, // required this.availWidth,
required this.maxImgSize, // required this.maxImgSize,
required this.imgWidget, // required this.imgWidget,
required this.playPauseController, // required this.playPauseController,
required this.percentageMiniplayer, // required this.percentageMiniplayer,
}); // });
final double availWidth; // final double availWidth;
final double maxImgSize; // final double maxImgSize;
final Widget imgWidget; // final Widget imgWidget;
final AnimationController playPauseController; // final AnimationController playPauseController;
/// 0 - 1, from minimized to when switched to expanded player // /// 0 - 1, from minimized to when switched to expanded player
/// // ///
/// by the time 1 is reached only image should be visible in the center of the widget // /// by the time 1 is reached only image should be visible in the center of the widget
final double percentageMiniplayer; // final double percentageMiniplayer;
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); // final player = ref.watch(audiobookPlayerProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider); // final currentChapter = ref.watch(currentPlayingChapterProvider);
final vanishingPercentage = 1 - percentageMiniplayer; // final vanishingPercentage = 1 - percentageMiniplayer;
// final progress = // // final progress =
// useStream(player.slowPositionStreamInBook, initialData: Duration.zero); // // useStream(player.slowPositionStreamInBook, initialData: Duration.zero);
final progress = // final progress =
useStream(player.positionStream, initialData: Duration.zero); // useStream(player.positionStream, initialData: Duration.zero);
final bookMetaExpanded = ref.watch(currentBookMetadataProvider); // final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
var barHeight = vanishingPercentage * 3; // var barHeight = vanishingPercentage * 3;
return Stack( // return Stack(
alignment: Alignment.topCenter, // alignment: Alignment.topCenter,
children: [ // children: [
Row( // Row(
children: [ // children: [
// image // // image
Padding( // Padding(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
left: ((availWidth - maxImgSize) / 2) * percentageMiniplayer, // left: ((availWidth - maxImgSize) / 2) * percentageMiniplayer,
), // ),
child: InkWell( // child: InkWell(
onTap: () { // onTap: () {
// navigate to item page // // navigate to item page
context.pushNamed( // context.pushNamed(
Routes.libraryItem.name, // Routes.libraryItem.name,
pathParameters: { // pathParameters: {
Routes.libraryItem.pathParamName!: // Routes.libraryItem.pathParamName!:
player.book!.libraryItemId, // player.book!.libraryItemId,
}, // },
); // );
}, // },
child: ConstrainedBox( // child: ConstrainedBox(
constraints: BoxConstraints( // constraints: BoxConstraints(
maxWidth: maxImgSize, // maxWidth: maxImgSize,
), // ),
child: imgWidget, // child: imgWidget,
), // ),
), // ),
), // ),
// author and title of the book // // author and title of the book
Expanded( // Expanded(
child: Padding( // child: Padding(
padding: const EdgeInsets.only(left: 8), // padding: const EdgeInsets.only(left: 8),
child: Column( // child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, // mainAxisSize: MainAxisSize.min,
children: [ // children: [
// AutoScrollText( // // AutoScrollText(
Text( // Text(
'${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}', // '${bookMetaExpanded?.title ?? ''} - ${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( // Text(
bookMetaExpanded?.authorName ?? '', // bookMetaExpanded?.authorName ?? '',
maxLines: 1, // maxLines: 1,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith( // style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context) // color: Theme.of(context)
.colorScheme // .colorScheme
.onSurface // .onSurface
.withValues(alpha: 0.7), // .withValues(alpha: 0.7),
), // ),
), // ),
], // ],
), // ),
), // ),
), // ),
// IconButton( // // IconButton(
// icon: const Icon(Icons.fullscreen), // // icon: const Icon(Icons.fullscreen),
// onPressed: () { // // onPressed: () {
// controller.animateToHeight(state: PanelState.MAX); // // controller.animateToHeight(state: PanelState.MAX);
// }, // // },
// ), // // ),
// rewind button // // rewind button
Opacity( // Opacity(
opacity: vanishingPercentage, // opacity: vanishingPercentage,
child: Padding( // child: Padding(
padding: const EdgeInsets.only(left: 8), // padding: const EdgeInsets.only(left: 8),
child: IconButton( // child: IconButton(
icon: const Icon( // icon: const Icon(
Icons.replay_30, // Icons.replay_30,
size: AppElementSizes.iconSizeSmall, // size: AppElementSizes.iconSizeSmall,
), // ),
onPressed: () {}, // onPressed: () {},
), // ),
), // ),
), // ),
// play/pause button // // play/pause button
Opacity( // Opacity(
opacity: vanishingPercentage, // opacity: vanishingPercentage,
child: Padding( // child: Padding(
padding: const EdgeInsets.only(right: 8), // padding: const EdgeInsets.only(right: 8),
child: AudiobookPlayerPlayPauseButton( // child: AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController, // playPauseController: playPauseController,
), // ),
), // ),
), // ),
], // ],
), // ),
SizedBox( // SizedBox(
height: barHeight, // height: barHeight,
child: LinearProgressIndicator( // child: LinearProgressIndicator(
// value: (progress.data ?? Duration.zero).inSeconds / // // value: (progress.data ?? Duration.zero).inSeconds /
// player.book!.duration.inSeconds, // // player.book!.duration.inSeconds,
value: (progress.data ?? Duration.zero).inSeconds / // value: (progress.data ?? Duration.zero).inSeconds /
(player.duration?.inSeconds ?? 1), // (player.duration?.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

@ -68,9 +68,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
return; return;
} }
if (isForward) { if (isForward) {
player.seekForward(); player.seekToNext();
} else { } else {
player.seekBackward(); player.seekToPrevious();
} }
}, },
); );

View file

@ -5,7 +5,7 @@ import 'package:vaani/features/player/providers/audiobook_player.dart'
show audiobookPlayerProvider; show audiobookPlayerProvider;
import 'package:vaani/features/player/providers/currently_playing_provider.dart' import 'package:vaani/features/player/providers/currently_playing_provider.dart'
show currentPlayingChapterProvider, currentlyPlayingBookProvider; show currentPlayingChapterProvider, currentlyPlayingBookProvider;
import 'package:vaani/features/player/view/player_when_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/main.dart' show appLogger; import 'package:vaani/main.dart' show appLogger;
@ -117,7 +117,8 @@ 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); // notifier.seekInBook(chapter.start + 90.ms);
notifier.skipToChapter(chapter.id);
notifier.play(); notifier.play();
}, },
); );

View file

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({
super.key,
this.iconSize = 48.0,
});
final double iconSize;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final playing = ref.watch(isPlayerPlayingProvider);
final playPauseController = useAnimationController(
duration: const Duration(milliseconds: 200),
initialValue: 1,
);
if (playing) {
playPauseController.forward();
} else {
playPauseController.reverse();
}
return switch (player.processingState) {
ProcessingState.loading || ProcessingState.buffering => const Padding(
padding: EdgeInsets.all(AppElementSizes.paddingRegular),
child: CircularProgressIndicator(),
),
ProcessingState.completed => IconButton(
onPressed: () async {
await player.seekInBook(const Duration(seconds: 0));
await player.play();
},
icon: const Icon(
Icons.replay,
),
),
ProcessingState.ready => IconButton(
onPressed: () async {
await player.togglePlayPause();
},
iconSize: iconSize,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: playPauseController,
),
),
ProcessingState.idle => const SizedBox.shrink(),
};
}
}

View file

@ -0,0 +1,82 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
class AudiobookChapterProgressBar extends HookConsumerWidget {
const AudiobookChapterProgressBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider);
final position = useStream(
player.positionStreamInBook,
initialData: const Duration(seconds: 0),
);
final buffered = useStream(
player.bufferedPositionStreamInBook,
initialData: const Duration(seconds: 0),
);
// now find the chapter that corresponds to the current time
// and calculate the progress of the current chapter
final currentChapterProgress = currentChapter == null
? null
: (player.positionInBook - currentChapter.start);
final currentChapterBuffered = currentChapter == null
? null
: (player.bufferedPositionInBook - currentChapter.start);
return ProgressBar(
progress:
currentChapterProgress ?? position.data ?? const Duration(seconds: 0),
total: currentChapter == null
? player.book?.duration ?? const Duration(seconds: 0)
: currentChapter.end - currentChapter.start,
// ! TODO add onSeek
onSeek: (duration) {
player.seekInBook(
duration + (currentChapter?.start ?? const Duration(seconds: 0)),
);
// player.seek(duration);
},
thumbRadius: 8,
buffered:
currentChapterBuffered ?? buffered.data ?? const Duration(seconds: 0),
bufferedBarColor: Theme.of(context).colorScheme.secondary,
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}
class AudiobookProgressBar extends HookConsumerWidget {
const AudiobookProgressBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final position = useStream(
player.slowPositionStreamInBook,
initialData: const Duration(seconds: 0),
);
return ProgressBar(
progress: position.data ?? const Duration(seconds: 0),
total: player.book?.duration ?? const Duration(seconds: 0),
thumbRadius: 8,
bufferedBarColor: Theme.of(context).colorScheme.secondary,
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}

View file

@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.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/view/player_when_expanded.dart'; import 'package:vaani/features/player/view/player_expanded.dart';
import 'package:vaani/features/player/view/widgets/speed_selector.dart'; import 'package:vaani/features/player/view/widgets/speed_selector.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/view/player_when_expanded.dart'; import 'package:vaani/features/player/view/player_expanded.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 {

View file

@ -1,18 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/audiobook_player.dart'; import 'package:vaani/features/player/core/audiobook_player.dart';
import 'package:vaani/shared/extensions/chapter.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 AudiobookPlayer player;
// id
int? chapterId;
// int _index; // int _index;
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
final throttler = Throttler(delay: Duration(seconds: 3)); final throttler = Throttler(delay: Duration(seconds: 3));
// final StreamController<PlaybackEvent> _playbackController = // final StreamController<PlaybackEvent> _playbackController =
// StreamController<PlaybackEvent>.broadcast(); // StreamController<PlaybackEvent>.broadcast();
SkipStartEnd({required this.start, required this.end, required this.player}) { SkipStartEnd({
required this.start,
required this.end,
required this.player,
this.chapterId,
}) {
// if (start > Duration()) { // if (start > Duration()) {
// _subscriptions.add( // _subscriptions.add(
// player.currentIndexStream.listen((index) { // player.currentIndexStream.listen((index) {
@ -25,25 +35,81 @@ class SkipStartEnd {
// }), // }),
// ); // );
// } // }
if (end > Duration()) { // 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.distinct().listen((position) { player.positionStream.listen((position) {
if (player.duration != null && final chapter = player.currentChapter;
player.duration!.inMilliseconds - player.position.inMilliseconds < if (chapter == null) {
end.inMilliseconds) { return;
throttler.call(() { }
print('跳过片尾'); if (chapter.id == chapterId) {
Future.microtask(() async { if (end > Duration.zero &&
await player.stop(); chapter.duration - (player.positionInBook - chapter.start) <
player.seekToNext(); 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;
} }
}), }),
); );
} }
} }
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();
}
}
/// dispose the timer /// dispose the timer
void dispose() { void dispose() {
for (var sub in _subscriptions) { for (var sub in _subscriptions) {
@ -53,38 +119,3 @@ class SkipStartEnd {
// _playbackController.close(); // _playbackController.close();
} }
} }
class Throttler {
final Duration delay;
Timer? _timer;
DateTime? _lastRun;
Throttler({required this.delay});
void call(void Function() callback) {
//
if (_lastRun == null) {
callback();
_lastRun = DateTime.now();
return;
}
//
if (DateTime.now().difference(_lastRun!) > delay) {
callback();
_lastRun = DateTime.now();
}
//
else {
_timer?.cancel();
_timer = Timer(delay, () {
callback();
_lastRun = DateTime.now();
});
}
}
void dispose() {
_timer?.cancel();
}
}

View file

@ -5,12 +5,13 @@ 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';
@Riverpod(keepAlive: true) @riverpod
class SkipStartEnd extends _$SkipStartEnd { class SkipStartEnd extends _$SkipStartEnd {
@override @override
core.SkipStartEnd? build() { core.SkipStartEnd? build() {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(simpleAudiobookPlayerProvider);
final bookId = player.book?.libraryItemId ?? '_'; final book = ref.watch(audiobookPlayerProvider.select((v) => v.book));
final bookId = book?.libraryItemId ?? '_';
if (bookId == '_') { if (bookId == '_') {
return null; return null;
} }
@ -18,6 +19,13 @@ class SkipStartEnd extends _$SkipStartEnd {
final start = bookSettings.playerSettings.skipChapterStart; final start = bookSettings.playerSettings.skipChapterStart;
final end = bookSettings.playerSettings.skipChapterEnd; final end = bookSettings.playerSettings.skipChapterEnd;
return core.SkipStartEnd(start: start, end: end, player: player); final skipStartEnd = core.SkipStartEnd(
start: start,
end: end,
player: player,
chapterId: player.currentChapter?.id,
);
ref.onDispose(skipStartEnd.dispose);
return skipStartEnd;
} }
} }

View file

@ -6,12 +6,12 @@ part of 'skip_start_end_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$skipStartEndHash() => r'202cfb36fdb3d3fa12debfb188f87650473a88a9'; String _$skipStartEndHash() => r'857b448eac9bb9ab85cea9217775712e660bc990';
/// See also [SkipStartEnd]. /// See also [SkipStartEnd].
@ProviderFor(SkipStartEnd) @ProviderFor(SkipStartEnd)
final skipStartEndProvider = final skipStartEndProvider =
NotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal( AutoDisposeNotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
SkipStartEnd.new, SkipStartEnd.new,
name: r'skipStartEndProvider', name: r'skipStartEndProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -20,6 +20,6 @@ final skipStartEndProvider =
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$SkipStartEnd = Notifier<core.SkipStartEnd?>; typedef _$SkipStartEnd = AutoDisposeNotifier<core.SkipStartEnd?>;
// ignore_for_file: type=lint // 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 // 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

@ -2,8 +2,7 @@ import 'package:flutter/material.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:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:vaani/features/player/view/player_expanded.dart';
import 'package:vaani/features/player/view/player_when_expanded.dart';
import 'package:vaani/features/player/view/widgets/speed_selector.dart'; import 'package:vaani/features/player/view/widgets/speed_selector.dart';
import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'; import 'package:vaani/features/sleep_timer/core/sleep_timer.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
@ -54,7 +53,7 @@ class SleepTimerButton extends HookConsumerWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: sleepTimer == null child: sleepTimer == null
? Icon( ? Icon(
Symbols.bedtime, Icons.bedtime_outlined,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
) )
: RemainingSleepTimeDisplay( : RemainingSleepTimeDisplay(
@ -153,7 +152,7 @@ class SleepTimerBottomSheet extends HookConsumerWidget {
onDurationSelected?.call(null); onDurationSelected?.call(null);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
icon: const Icon(Symbols.bedtime_off), icon: const Icon(Icons.bedtime_off_outlined),
label: const Text('Cancel Sleep Timer'), label: const Text('Cancel Sleep Timer'),
), ),
), ),

View file

@ -12,6 +12,7 @@ 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'
show audiobookPlayerProvider, simpleAudiobookPlayerProvider; show audiobookPlayerProvider, simpleAudiobookPlayerProvider;
import 'package:vaani/features/shake_detection/providers/shake_detector.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/features/sleep_timer/providers/sleep_timer_provider.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/models/tray.dart'; import 'package:vaani/models/tray.dart';
@ -189,7 +190,7 @@ class _EagerInitialization extends ConsumerWidget {
ref.watch(playbackReporterProvider); ref.watch(playbackReporterProvider);
ref.watch(simpleDownloadManagerProvider); ref.watch(simpleDownloadManagerProvider);
ref.watch(shakeDetectorProvider); ref.watch(shakeDetectorProvider);
// ref.watch(skipStartEndProvider); ref.watch(skipStartEndProvider);
} catch (e) { } catch (e) {
debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); debugPrintStack(stackTrace: StackTrace.current, label: e.toString());
appLogger.severe(e.toString()); appLogger.severe(e.toString());

View file

@ -43,7 +43,7 @@ class Routes {
parentRoute: settings, parentRoute: settings,
); );
static const playerSettings = _SimpleRoute( static const playerSettings = _SimpleRoute(
pathName: 'player', pathName: 'playerSettings',
name: 'playerSettings', name: 'playerSettings',
parentRoute: settings, parentRoute: settings,
); );
@ -101,6 +101,12 @@ class Routes {
parentRoute: onboarding, parentRoute: onboarding,
); );
// player page
static const player = _SimpleRoute(
pathName: 'player',
name: 'player',
);
// logs page // logs page
static const logs = _SimpleRoute( static const logs = _SimpleRoute(
pathName: 'logs', pathName: 'logs',

View file

@ -8,6 +8,7 @@ import 'package:vaani/features/library_browser/view/library_browser_page.dart';
import 'package:vaani/features/logging/view/logs_page.dart'; import 'package:vaani/features/logging/view/logs_page.dart';
import 'package:vaani/features/onboarding/view/callback_page.dart'; import 'package:vaani/features/onboarding/view/callback_page.dart';
import 'package:vaani/features/onboarding/view/onboarding_single_page.dart'; import 'package:vaani/features/onboarding/view/onboarding_single_page.dart';
import 'package:vaani/features/player/view/player_expanded.dart';
import 'package:vaani/features/you/view/server_manager.dart'; import 'package:vaani/features/you/view/server_manager.dart';
import 'package:vaani/features/you/view/you_page.dart'; import 'package:vaani/features/you/view/you_page.dart';
import 'package:vaani/main.dart'; import 'package:vaani/main.dart';
@ -234,6 +235,13 @@ class MyAppRouter {
], ],
), ),
// loggers page
GoRoute(
path: Routes.player.localPath,
name: Routes.player.name,
pageBuilder: defaultPageBuilder(const PlayerExpanded()),
),
// loggers page // loggers page
GoRoute( GoRoute(
path: Routes.logs.localPath, path: Routes.logs.localPath,

View file

@ -1,19 +1,16 @@
import 'dart:io';
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:miniplayer/miniplayer.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/view/audiobook_player.dart'; import 'package:vaani/features/player/view/player_minimized.dart';
import 'package:vaani/features/player/view/player_when_expanded.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';
import 'package:vaani/main.dart'; import 'package:vaani/main.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons;
import 'package:vaani/shared/utils/utils.dart';
// stack to track changes in navigationShell.currentIndex // stack to track changes in navigationShell.currentIndex
// home is always at index 0 and at the start and should be the last before popping // home is always at index 0 and at the start and should be the last before popping
@ -37,73 +34,21 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final playerProgress = ref.watch(playerHeightProvider);
final isMobile = Platform.isAndroid || Platform.isIOS || Platform.isFuchsia;
final isVertical = size.height > size.width; final isVertical = size.height > size.width;
onBackButtonPressed() async {
final isPlayerExpanded = playerProgress != playerMinHeight;
appLogger.fine( return Scaffold(
'BackButtonListener: Back button pressed, isPlayerExpanded: $isPlayerExpanded, stack: $navigationShellStack, pendingPlayerModals: $pendingPlayerModals', body: Stack(
); alignment: Alignment.bottomCenter,
children: [
// close miniplayer if it is open Utils.isMobile() || isVertical
if (isPlayerExpanded && pendingPlayerModals == 0) { ? navigationShell
appLogger.fine( : buildNavLeft(context, ref),
'BackButtonListener: closing the player', // const AudiobookPlayer(),
); const PlayerMinimized(),
audioBookMiniplayerController.animateToHeight(state: PanelState.MIN); ],
return true;
}
// do the the following only if the current branch has nothing to pop
final canPop = GoRouter.of(context).canPop();
if (canPop) {
appLogger.fine(
'BackButtonListener: passing it to the router as canPop is true',
);
return false;
}
if (navigationShellStack.isNotEmpty) {
// pop the last index from the stack and navigate to it
final index = navigationShellStack.last;
navigationShellStack.remove(index);
appLogger.fine('BackButtonListener: popping the stack, index: $index');
// if the stack is empty, navigate to home else navigate to the last index
if (navigationShellStack.isNotEmpty) {
navigationShell.goBranch(navigationShellStack.last);
return true;
}
}
if (navigationShell.currentIndex != 0) {
// if the stack is empty and the current branch is not home, navigate to home
appLogger.fine('BackButtonListener: navigating to home');
navigationShell.goBranch(0);
return true;
}
appLogger.fine('BackButtonListener: passing it to the router');
return false;
}
// TODO: Implement a better way to handle back button presses to minimize player
return BackButtonListener(
onBackButtonPressed: onBackButtonPressed,
child: Scaffold(
body: Stack(
children: [
isMobile || isVertical
? navigationShell
: buildNavLeft(context, ref),
const AudiobookPlayer(),
],
),
bottomNavigationBar:
isMobile || isVertical ? buildNavBottom(context, ref) : null,
), ),
bottomNavigationBar:
Utils.isMobile() || isVertical ? buildNavBottom(context, ref) : null,
); );
} }
@ -116,7 +61,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
SafeArea( SafeArea(
child: NavigationRail( child: NavigationRail(
minWidth: 60, minWidth: 60,
minExtendedWidth: 120, minExtendedWidth: 180,
extended: MediaQuery.of(context).size.width > 640, extended: MediaQuery.of(context).size.width > 640,
// extended: false, // extended: false,
destinations: _navigationItems(context).map((item) { destinations: _navigationItems(context).map((item) {

View file

@ -6,7 +6,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.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:material_symbols_icons/symbols.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.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';
@ -71,8 +70,8 @@ class AppSettingsPage extends HookConsumerWidget {
title: Text(S.of(context).autoTurnOnSleepTimer), title: Text(S.of(context).autoTurnOnSleepTimer),
description: Text(S.of(context).automaticallyDescription), description: Text(S.of(context).automaticallyDescription),
leading: sleepTimerSettings.autoTurnOnTimer leading: sleepTimerSettings.autoTurnOnTimer
? const Icon(Symbols.time_auto, fill: 1) ? const Icon(Icons.timer, fill: 1)
: const Icon(Symbols.timer_off, fill: 1), : const Icon(Icons.timer_off, fill: 1),
onPressed: (context) { onPressed: (context) {
context.pushNamed(Routes.autoSleepTimerSettings.name); context.pushNamed(Routes.autoSleepTimerSettings.name);
}, },

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/settings/view/simple_settings_page.dart';
@ -38,8 +37,8 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
S.of(context).autoTurnOnTimerDescription, S.of(context).autoTurnOnTimerDescription,
), ),
leading: sleepTimerSettings.autoTurnOnTimer leading: sleepTimerSettings.autoTurnOnTimer
? const Icon(Symbols.time_auto) ? const Icon(Icons.timer_outlined)
: const Icon(Symbols.timer_off), : const Icon(Icons.timer_off_outlined),
onToggle: (value) { onToggle: (value) {
ref.read(appSettingsProvider.notifier).update( ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.sleepTimerSettings( appSettings.copyWith.sleepTimerSettings(
@ -52,7 +51,7 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
// auto turn on time settings, enabled only when autoTurnOnTimer is enabled // auto turn on time settings, enabled only when autoTurnOnTimer is enabled
SettingsTile.navigation( SettingsTile.navigation(
enabled: enabled, enabled: enabled,
leading: const Icon(Symbols.timer_play), leading: const Icon(Icons.play_circle),
title: Text(S.of(context).autoTurnOnTimerFrom), title: Text(S.of(context).autoTurnOnTimerFrom),
description: Text( description: Text(
S.of(context).autoTurnOnTimerFromDescription, S.of(context).autoTurnOnTimerFromDescription,
@ -78,7 +77,7 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
), ),
SettingsTile.navigation( SettingsTile.navigation(
enabled: enabled, enabled: enabled,
leading: const Icon(Symbols.timer_pause), leading: const Icon(Icons.pause_circle),
title: Text(S.of(context).autoTurnOnTimerUntil), title: Text(S.of(context).autoTurnOnTimerUntil),
description: Text( description: Text(
S.of(context).autoTurnOnTimerUntilDescription, S.of(context).autoTurnOnTimerUntilDescription,
@ -107,7 +106,7 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
// switch tile for always auto turn on timer no matter what // switch tile for always auto turn on timer no matter what
SettingsTile.switchTile( SettingsTile.switchTile(
leading: const Icon(Symbols.all_inclusive), leading: const Icon(Icons.all_inclusive),
title: Text(S.of(context).autoTurnOnTimerAlways), title: Text(S.of(context).autoTurnOnTimerAlways),
description: Text( description: Text(
S.of(context).autoTurnOnTimerAlwaysDescription, S.of(context).autoTurnOnTimerAlwaysDescription,

View file

@ -0,0 +1,37 @@
import 'dart:async';
//
class Throttler {
final Duration delay;
Timer? _timer;
DateTime? _lastRun;
Throttler({required this.delay});
void call(void Function() callback) {
//
if (_lastRun == null) {
callback();
_lastRun = DateTime.now();
return;
}
//
if (DateTime.now().difference(_lastRun!) > delay) {
callback();
_lastRun = DateTime.now();
}
//
else {
_timer?.cancel();
_timer = Timer(delay, () {
callback();
_lastRun = DateTime.now();
});
}
}
void dispose() {
_timer?.cancel();
}
}

View file

@ -222,14 +222,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
chalkdart:
dependency: transitive
description:
name: chalkdart
sha256: "7ffc6bd39c81453fb9ba8dbce042a9c960219b75ea1c07196a7fa41c2fab9e86"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -326,14 +318,6 @@ 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:
@ -803,14 +787,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:
@ -907,14 +883,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.11.1"
material_symbols_icons:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: "9a7de58ffc299c8e362b4e860e36e1d198fa0981a894376fe1b6bfe52773e15b"
url: "https://pub.dev"
source: hosted
version: "4.2874.0"
media_kit: media_kit:
dependency: transitive dependency: transitive
description: description:
@ -931,6 +899,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:
@ -955,15 +931,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
miniplayer:
dependency: "direct main"
description:
path: "."
ref: feat-notifier-for-percent-dismissed
resolved-ref: "480f7933deaf0225ceb3a97162efca53610ba840"
url: "https://github.com/Dr-Blank/miniplayer.git"
source: git
version: "1.0.3"
numberpicker: numberpicker:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -42,7 +42,7 @@ 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
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
@ -63,26 +63,26 @@ dependencies:
isar_flutter_libs: ^4.0.0-dev.13 isar_flutter_libs: ^4.0.0-dev.13
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
lottie: ^3.1.0 lottie: ^3.1.0
material_color_utilities: ^0.11.1 material_color_utilities: ^0.11.1
material_symbols_icons: ^4.2785.1 # material_symbols_icons: ^4.2785.1
miniplayer: # miniplayer:
git: # git:
url: https://github.com/Dr-Blank/miniplayer.git # url: https://github.com/Dr-Blank/miniplayer.git
ref: feat-notifier-for-percent-dismissed # ref: feat-notifier-for-percent-dismissed
numberpicker: ^2.1.2 numberpicker: ^2.1.2
package_info_plus: ^8.0.0 package_info_plus: ^8.0.0
path: ^1.9.0 path: ^1.9.0

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