替换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

@ -8,13 +8,19 @@ import 'package:just_audio/just_audio.dart';
import 'package:just_audio_background/just_audio_background.dart';
import 'package:logging/logging.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/models/app_settings.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
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]
Duration sumOfTracks(BookExpanded book, int? index) {
_logger.fine('Calculating sum of tracks for index: $index');
@ -31,31 +37,17 @@ Duration sumOfTracks(BookExpanded book, int? index) {
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
class AudiobookPlayer extends AudioPlayer {
// constructor which takes in the BookExpanded object
AudiobookPlayer(this.token, this.baseUrl) : super() {
// set the source of the player to the first track in the book
_logger.config('Setting up audiobook player');
playerStateStream.listen((playerState) {
if (playerState.processingState == ProcessingState.completed) {
Future.microtask(seekToNext);
}
});
// playerStateStream.listen((playerState) {
// if (playerState.processingState == ProcessingState.completed) {
// Future.microtask(seekToNext);
// }
// });
}
/// the [BookExpanded] being played
@ -76,17 +68,16 @@ class AudiobookPlayer extends AudioPlayer {
final Uri baseUrl;
// the current index of the audio file in the [book]
int _currentIndex = 0;
// int _currentIndex = 0;
// available audio tracks
int? get availableTracks => _book?.tracks.length;
List<Uri>? _downloadedUris;
/// sets the current [AudioTrack] as the source of the player
Future<void> setSourceAudiobook(
BookExpanded? book, {
bool preload = true,
// int? initialIndex,
int? initialIndex,
Duration? initialPosition,
List<Uri>? downloadedUris,
Uri? artworkUri,
@ -94,7 +85,7 @@ class AudiobookPlayer extends AudioPlayer {
_logger.finer(
'Initial position: $initialPosition, Downloaded URIs: $downloadedUris',
);
// final appSettings = loadOrCreateAppSettings();
final appSettings = loadOrCreateAppSettings();
if (book == null) {
_book = null;
_logger.info('Book is null, stopping player');
@ -111,103 +102,52 @@ class AudiobookPlayer extends AudioPlayer {
await stop();
_book = book;
_downloadedUris = downloadedUris;
// some calculations to set the initial index and position
// 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
// 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
// initialPosition ;
final trackToPlay = getTrackToPlay(book, initialPosition ?? Duration.zero);
final trackToPlay =
_book!.findTrackAtTime(initialPosition ?? Duration.zero);
final initialIndex = book.tracks.indexOf(trackToPlay);
final initialPositionInTrack = initialPosition != null
? initialPosition - trackToPlay.startOffset
: null;
await setAudioSourceTrack(
initialIndex,
initialPosition: initialPositionInTrack,
);
// _logger.finer('Setting audioSource');
// await setAudioSource(
// preload: preload,
// initialIndex: initialIndex,
// initialPosition: initialPositionInTrack,
// 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,
),
_logger.finer('Setting audioSource');
final playlist = 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}',
id: book.libraryItemId + track.index.toString(),
// Metadata to display in the notification:
title: appSettings.notificationSettings.primaryTitle
.formatNotificationTitle(book!),
.formatNotificationTitle(book),
album: appSettings.notificationSettings.secondaryTitle
.formatNotificationTitle(book!),
artUri: Uri.parse(
'$baseUrl/api/items/${book?.libraryItemId}/cover?token=$token&width=800',
),
.formatNotificationTitle(book),
artUri: artworkUri ??
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
@ -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
/// 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
Future<void> seekInBook(Duration? positionInBook, {int? index}) async {
Future<void> seekInBook(Duration globalPosition) async {
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 track = _book!.findTrackAtTime(globalPosition);
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);
final i = tracks.indexOf(trackToPlay);
final positionInTrack = positionInBook - trackToPlay.startOffset;
return setAudioSourceTrack(i, initialPosition: positionInTrack);
// return super.seek(positionInTrack, index: i);
//
if (index != currentIndex) {
await seek(positionInTrack, index: index);
}
await seek(positionInTrack);
}
// 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 doNotSeekBackIfLessThan = Duration(seconds: 5);
/// seek forward to the next chapter
void seekForward() {
seekInBook(currentChapter!.end + offset);
// final index = _book!.chapters.indexOf(currentChapter!);
// if (index < _book!.chapters.length - 1) {
// 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);
final chapter = _book!.chapters.firstWhere(
(ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
);
if (position != null) {
print('章节开头: ${chapter.start}');
print('章节开头: ${chapter.start + position}');
await seekInBook(chapter.start + position);
return;
}
BookChapter chapterToSeekTo =
_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,
// );
await seekInBook(chapter.start + offset);
}
@override
Future<void> seekToNext() {
if (_currentIndex >= availableTracks!) {
return super.seek(duration);
Future<void> seekToNext() async {
if (_book == null) {
// 退
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
Future<void> seekToPrevious() {
if (_currentIndex == 0) {
return super.seek(Duration());
Future<void> seekToPrevious() async {
if (_book == null) {
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
Duration get positionInBook {
if (_book == null) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[_currentIndex].startOffset;
return position + _book!.tracks[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
Duration get bufferedPositionInBook {
if (_book == null) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return bufferedPosition + _book!.tracks[_currentIndex].startOffset;
return bufferedPosition + _book!.tracks[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
// - positionStream
// - bufferedPositionStream
Stream<Duration> get positionStreamInBook {
// return the positionInBook stream
return super.positionStream.map((position) {
if (_book == null) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[_currentIndex].startOffset;
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
});
}
Stream<Duration> get bufferedPositionStreamInBook {
return super.bufferedPositionStream.map((position) {
if (_book == null) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[_currentIndex].startOffset;
return position + _book!.tracks[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
return superPositionStream.map((position) {
if (_book == null) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[_currentIndex].startOffset;
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
});
}
@ -388,17 +319,7 @@ class AudiobookPlayer extends AudioPlayer {
if (_book == null) {
return null;
}
// if the list is empty, return null
if (_book!.chapters.isEmpty) {
return null;
}
return _book!.chapters.firstWhere(
(element) {
return element.start <= positionInBook &&
element.end >= positionInBook + offset;
},
orElse: () => _book!.chapters.first,
);
return _book!.findChapterAtTime(positionInBook);
}
}
@ -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 {
// for playing audio on windows, linux
JustAudioMediaKit.ensureInitialized(windows: false);
JustAudioMediaKit.ensureInitialized();
// for configuring how this app will interact with other audio apps
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:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/api_provider.dart';
@ -51,3 +52,12 @@ class AudiobookPlayer extends _$AudiobookPlayer {
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
// **************************************************************************
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() =>
r'5e94bbff4314adceb5affa704fc4d079d4016afa';

View file

@ -4,7 +4,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.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:vaani/features/player/providers/audiobook_player.dart';
@ -60,7 +60,7 @@ double playerHeight(
return playerExpandProgress.value;
}
final audioBookMiniplayerController = MiniplayerController();
// final audioBookMiniplayerController = MiniplayerController();
@Riverpod(keepAlive: true)
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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:miniplayer/miniplayer.dart';
import 'package:vaani/api/image_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/currently_playing_provider.dart';
import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/inverse_lerp.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_hooks/flutter_hooks.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:just_audio/just_audio.dart';
// import 'package:miniplayer/miniplayer.dart';
// import 'package:vaani/api/image_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/currently_playing_provider.dart';
// import 'package:vaani/features/player/providers/player_form.dart';
// import 'package:vaani/settings/app_settings_provider.dart';
// import 'package:vaani/shared/extensions/inverse_lerp.dart';
// import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
// import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
import 'player_when_expanded.dart';
import 'player_when_minimized.dart';
// import 'player_when_expanded.dart';
// import 'player_when_minimized.dart';
const playerMaxHeightPercentOfScreen = 0.8;
// const playerMaxHeightPercentOfScreen = 0.8;
class AudiobookPlayer extends HookConsumerWidget {
const AudiobookPlayer({super.key});
// class AudiobookPlayer extends HookConsumerWidget {
// const AudiobookPlayer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final currentBook = ref.watch(currentlyPlayingBookProvider);
if (currentBook == null) {
return const SizedBox.shrink();
}
final itemBeingPlayed =
ref.watch(libraryItemProvider(currentBook.libraryItemId));
final player = ref.watch(audiobookPlayerProvider);
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();
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final appSettings = ref.watch(appSettingsProvider);
// final currentBook = ref.watch(currentlyPlayingBookProvider);
// if (currentBook == null) {
// return const SizedBox.shrink();
// }
// final itemBeingPlayed =
// ref.watch(libraryItemProvider(currentBook.libraryItemId));
// final player = ref.watch(audiobookPlayerProvider);
// 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 playPauseController = useAnimationController(
duration: const Duration(milliseconds: 200),
initialValue: 1,
);
// final playPauseController = useAnimationController(
// duration: const Duration(milliseconds: 200),
// initialValue: 1,
// );
// add controller to the player state listener
player.playerStateStream.listen((state) {
if (state.playing) {
playPauseController.forward();
} else {
playPauseController.reverse();
}
});
// // add controller to the player state listener
// player.playerStateStream.listen((state) {
// if (state.playing) {
// playPauseController.forward();
// } else {
// playPauseController.reverse();
// }
// });
// theme from image
final imageTheme = ref.watch(
themeOfLibraryItemProvider(
itemBeingPlayed.valueOrNull?.id,
brightness: Theme.of(context).brightness,
highContrast: appSettings.themeSettings.highContrast ||
MediaQuery.of(context).highContrast,
),
);
// // theme from image
// final imageTheme = ref.watch(
// themeOfLibraryItemProvider(
// itemBeingPlayed.valueOrNull?.id,
// brightness: Theme.of(context).brightness,
// highContrast: appSettings.themeSettings.highContrast ||
// MediaQuery.of(context).highContrast,
// ),
// );
// max height of the player is the height of the screen
final playerMaxHeight = MediaQuery.of(context).size.height;
// // 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;
// final availWidth = MediaQuery.of(context).size.width;
// the image width when the player is expanded
final maxImgSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
// // the image width when the player is expanded
// final maxImgSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
final preferredVolume = appSettings.playerSettings.preferredDefaultVolume;
return Theme(
data: ThemeData(
colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
),
child: Miniplayer(
valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
onDragDown: (percentage) async {
// preferred volume
// set volume to 0 when dragging down
await player
.setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
},
minHeight: playerMinHeight,
// subtract the height of notches and other system UI
maxHeight: playerMaxHeight,
controller: audioBookMiniplayerController,
elevation: 4,
// duration: Duration(seconds: 3),
onDismissed: () {
// add a delay before closing the player
// to allow the user to see the player closing
Future.delayed(const Duration(milliseconds: 300), () {
player.setSourceAudiobook(null);
});
},
curve: Curves.linear,
builder: (height, percentage) {
// 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
final miniplayerPercentageDeclaration =
(maxImgSize - playerMinHeight) /
(playerMaxHeight - playerMinHeight);
final bool isFormMiniplayer =
percentage < miniplayerPercentageDeclaration;
// final preferredVolume = appSettings.playerSettings.preferredDefaultVolume;
// return Theme(
// data: ThemeData(
// colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
// ),
// child: Miniplayer(
// valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
// onDragDown: (percentage) async {
// // preferred volume
// // set volume to 0 when dragging down
// await player
// .setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
// },
// minHeight: playerMinHeight,
// // subtract the height of notches and other system UI
// maxHeight: playerMaxHeight,
// controller: audioBookMiniplayerController,
// elevation: 4,
// // duration: Duration(seconds: 3),
// onDismissed: () {
// // add a delay before closing the player
// // to allow the user to see the player closing
// Future.delayed(const Duration(milliseconds: 300), () {
// player.setSourceAudiobook(null);
// });
// },
// curve: Curves.linear,
// builder: (height, percentage) {
// // 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
// final miniplayerPercentageDeclaration =
// (maxImgSize - playerMinHeight) /
// (playerMaxHeight - playerMinHeight);
// final bool isFormMiniplayer =
// percentage < miniplayerPercentageDeclaration;
if (!isFormMiniplayer) {
// this calculation needs a refactor
var percentageExpandedPlayer = percentage
.inverseLerp(
miniplayerPercentageDeclaration,
1,
)
.clamp(0.0, 1.0);
// if (!isFormMiniplayer) {
// // this calculation needs a refactor
// var percentageExpandedPlayer = percentage
// .inverseLerp(
// miniplayerPercentageDeclaration,
// 1,
// )
// .clamp(0.0, 1.0);
return PlayerWhenExpanded(
imageSize: maxImgSize,
img: imgWidget,
percentageExpandedPlayer: percentageExpandedPlayer,
playPauseController: playPauseController,
);
}
// return PlayerWhenExpanded(
// imageSize: maxImgSize,
// img: imgWidget,
// percentageExpandedPlayer: percentageExpandedPlayer,
// playPauseController: playPauseController,
// );
// }
//Miniplayer
final percentageMiniplayer = percentage.inverseLerp(
0,
miniplayerPercentageDeclaration,
);
// //Miniplayer
// final percentageMiniplayer = percentage.inverseLerp(
// 0,
// miniplayerPercentageDeclaration,
// );
return PlayerWhenMinimized(
maxImgSize: maxImgSize,
availWidth: availWidth,
imgWidget: imgWidget,
playPauseController: playPauseController,
percentageMiniplayer: percentageMiniplayer,
);
},
),
);
}
}
// return PlayerWhenMinimized(
// maxImgSize: maxImgSize,
// availWidth: availWidth,
// imgWidget: imgWidget,
// playPauseController: playPauseController,
// percentageMiniplayer: percentageMiniplayer,
// );
// },
// ),
// );
// }
// }
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({
super.key,
required this.playPauseController,
this.iconSize = 48.0,
});
// class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
// const AudiobookPlayerPlayPauseButton({
// super.key,
// required this.playPauseController,
// this.iconSize = 48.0,
// });
final double iconSize;
final AnimationController playPauseController;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
// final double iconSize;
// final AnimationController playPauseController;
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final player = ref.watch(audiobookPlayerProvider);
return switch (player.processingState) {
ProcessingState.loading || ProcessingState.buffering => const Padding(
padding: EdgeInsets.all(8.0),
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(),
};
}
}
// return switch (player.processingState) {
// ProcessingState.loading || ProcessingState.buffering => const Padding(
// padding: EdgeInsets.all(8.0),
// 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(),
// };
// }
// }
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,
);
}
}
// ! TODO remove onTap
void onTap() {}
// // ! 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:miniplayer/miniplayer.dart';
import 'package:vaani/constants/sizes.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/view/audiobook_player.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/extensions/inverse_lerp.dart';
import 'package:vaani/shared/widgets/not_implemented.dart';
// import 'package:flutter/material.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:miniplayer/miniplayer.dart';
// import 'package:vaani/constants/sizes.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/view/audiobook_player.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/extensions/inverse_lerp.dart';
// import 'package:vaani/shared/widgets/not_implemented.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';
// 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;
// var pendingPlayerModals = 0;
class PlayerWhenExpanded extends HookConsumerWidget {
const PlayerWhenExpanded({
super.key,
required this.imageSize,
required this.img,
required this.percentageExpandedPlayer,
required this.playPauseController,
});
// class PlayerWhenExpanded extends HookConsumerWidget {
// const PlayerWhenExpanded({
// super.key,
// required this.imageSize,
// required this.img,
// required this.percentageExpandedPlayer,
// required this.playPauseController,
// });
/// padding values control the position of the image
final double imageSize;
final Widget img;
final double percentageExpandedPlayer;
final AnimationController playPauseController;
// /// padding values control the position of the image
// final double imageSize;
// final Widget img;
// final double percentageExpandedPlayer;
// final AnimationController playPauseController;
@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%
const lateStart = 0.4;
const earlyEnd = 1;
final earlyPercentage = percentageExpandedPlayer
.inverseLerp(
lateStart,
earlyEnd,
)
.clamp(0.0, 1.0);
final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
// @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%
// const lateStart = 0.4;
// const earlyEnd = 1;
// final earlyPercentage = percentageExpandedPlayer
// .inverseLerp(
// lateStart,
// earlyEnd,
// )
// .clamp(0.0, 1.0);
// final currentChapter = ref.watch(currentPlayingChapterProvider);
// final currentBookMetadata = ref.watch(currentBookMetadataProvider);
return Column(
children: [
// sized box for system status bar; not needed as not full screen
SizedBox(
height: MediaQuery.of(context).padding.top * earlyPercentage,
),
// return Column(
// children: [
// // sized box for system status bar; not needed as not full screen
// SizedBox(
// 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
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100 * earlyPercentage,
),
child: Opacity(
opacity: earlyPercentage,
child: Padding(
padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// the down arrow
IconButton(
iconSize: 30,
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () {
// minimize the player
audioBookMiniplayerController.animateToHeight(
state: PanelState.MIN,
);
},
),
// // a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button
// ConstrainedBox(
// constraints: BoxConstraints(
// maxHeight: 100 * earlyPercentage,
// ),
// child: Opacity(
// opacity: earlyPercentage,
// child: Padding(
// padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.max,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// // the down arrow
// IconButton(
// iconSize: 30,
// icon: const Icon(Icons.keyboard_arrow_down),
// onPressed: () {
// // minimize the player
// audioBookMiniplayerController.animateToHeight(
// state: PanelState.MIN,
// );
// },
// ),
// the cast button
IconButton(
icon: const Icon(Icons.cast),
onPressed: () {
showNotImplementedToast(context);
},
),
],
),
),
),
),
// // the cast button
// IconButton(
// icon: const Icon(Icons.cast),
// onPressed: () {
// showNotImplementedToast(context);
// },
// ),
// ],
// ),
// ),
// ),
// ),
// the image
Padding(
padding: EdgeInsets.only(
top: AppElementSizes.paddingLarge * earlyPercentage,
),
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 * earlyPercentage,
spreadRadius: 8 * earlyPercentage,
// offset: Offset(0, 16 * earlyPercentage),
),
],
),
child: SizedBox(
height: imageSize,
child: InkWell(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular * earlyPercentage,
),
child: img,
),
),
),
),
),
),
// // the image
// Padding(
// padding: EdgeInsets.only(
// top: AppElementSizes.paddingLarge * earlyPercentage,
// ),
// 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 * earlyPercentage,
// spreadRadius: 8 * earlyPercentage,
// // offset: Offset(0, 16 * earlyPercentage),
// ),
// ],
// ),
// child: SizedBox(
// height: imageSize,
// child: InkWell(
// onTap: () {},
// child: ClipRRect(
// borderRadius: BorderRadius.circular(
// AppElementSizes.borderRadiusRegular * earlyPercentage,
// ),
// child: img,
// ),
// ),
// ),
// ),
// ),
// ),
// the chapter title
Expanded(
child: Opacity(
opacity: earlyPercentage,
child: Padding(
padding: EdgeInsets.only(
top: AppElementSizes.paddingRegular * earlyPercentage,
// horizontal: 16.0,
),
// child: SizedBox(
// same as the image width
// width: imageSize,
child: currentChapter == null
? const SizedBox()
: Text(
currentChapter.title,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// ),
),
),
),
// // the chapter title
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: Padding(
// padding: EdgeInsets.only(
// top: AppElementSizes.paddingRegular * earlyPercentage,
// // horizontal: 16.0,
// ),
// // child: SizedBox(
// // same as the image width
// // width: imageSize,
// 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: Opacity(
opacity: earlyPercentage,
child: Padding(
padding: EdgeInsets.only(
bottom: AppElementSizes.paddingRegular * earlyPercentage,
// horizontal: 16.0,
),
// child: SizedBox(
// same as the image width
// width: imageSize,
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 book name and author
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: Padding(
// padding: EdgeInsets.only(
// bottom: AppElementSizes.paddingRegular * earlyPercentage,
// // horizontal: 16.0,
// ),
// // child: SizedBox(
// // same as the image width
// // width: imageSize,
// 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: Opacity(
opacity: earlyPercentage,
child: SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
// top: AppElementSizes.paddingRegular * earlyPercentage,
left: AppElementSizes.paddingRegular * earlyPercentage,
right: AppElementSizes.paddingRegular * earlyPercentage,
),
child: const AudiobookChapterProgressBar(),
),
),
),
),
// // the progress bar
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// width: imageSize,
// child: Padding(
// padding: EdgeInsets.only(
// // top: AppElementSizes.paddingRegular * earlyPercentage,
// left: AppElementSizes.paddingRegular * earlyPercentage,
// right: AppElementSizes.paddingRegular * earlyPercentage,
// ),
// child: const AudiobookChapterProgressBar(),
// ),
// ),
// ),
// ),
Expanded(
child: Opacity(
opacity: earlyPercentage,
child: SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
// top: AppElementSizes.paddingRegular * earlyPercentage,
left: AppElementSizes.paddingRegular * earlyPercentage,
right: AppElementSizes.paddingRegular * earlyPercentage,
),
child: const AudiobookProgressBar(),
),
),
),
),
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// width: imageSize,
// child: Padding(
// padding: EdgeInsets.only(
// // top: AppElementSizes.paddingRegular * earlyPercentage,
// left: AppElementSizes.paddingRegular * earlyPercentage,
// right: AppElementSizes.paddingRegular * earlyPercentage,
// ),
// child: const AudiobookProgressBar(),
// ),
// ),
// ),
// ),
// the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
Expanded(
flex: 2,
child: Opacity(
opacity: earlyPercentage,
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(
playPauseController: playPauseController,
),
// buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true),
// next chapter
const AudiobookPlayerSeekChapterButton(isForward: true),
],
),
),
),
),
// // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
// Expanded(
// flex: 2,
// child: Opacity(
// opacity: earlyPercentage,
// 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(
// playPauseController: playPauseController,
// ),
// // buttonSkipForwards
// const AudiobookPlayerSeekButton(isForward: true),
// // next chapter
// const AudiobookPlayerSeekChapterButton(isForward: true),
// ],
// ),
// ),
// ),
// ),
// speed control, sleep timer, chapter list, and settings
Expanded(
child: Opacity(
opacity: earlyPercentage,
child: SizedBox(
// padding: EdgeInsets.only(
// bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage,
// ),
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(),
// settings
// IconButton(
// icon: const Icon(Icons.more_horiz),
// onPressed: () {
// // show toast
// showNotImplementedToast(context);
// },
// ),
],
),
),
),
),
],
);
}
}
// // speed control, sleep timer, chapter list, and settings
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// // padding: EdgeInsets.only(
// // bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage,
// // ),
// 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(),
// // settings
// // IconButton(
// // icon: const Icon(Icons.more_horiz),
// // onPressed: () {
// // // show toast
// // showNotImplementedToast(context);
// // },
// // ),
// ],
// ),
// ),
// ),
// ),
// ],
// );
// }
// }

View file

@ -1,155 +1,155 @@
import 'package:flutter/material.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/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/audiobook_player.dart';
import 'package:vaani/router/router.dart';
// import 'package:flutter/material.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/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/audiobook_player.dart';
// import 'package:vaani/router/router.dart';
class PlayerWhenMinimized extends HookConsumerWidget {
const PlayerWhenMinimized({
super.key,
required this.availWidth,
required this.maxImgSize,
required this.imgWidget,
required this.playPauseController,
required this.percentageMiniplayer,
});
// class PlayerWhenMinimized extends HookConsumerWidget {
// const PlayerWhenMinimized({
// super.key,
// required this.availWidth,
// required this.maxImgSize,
// required this.imgWidget,
// required this.playPauseController,
// required this.percentageMiniplayer,
// });
final double availWidth;
final double maxImgSize;
final Widget imgWidget;
final AnimationController playPauseController;
// final double availWidth;
// final double maxImgSize;
// final Widget imgWidget;
// final AnimationController playPauseController;
/// 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
final double percentageMiniplayer;
// /// 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
// final double percentageMiniplayer;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final player = ref.watch(audiobookPlayerProvider);
// final currentChapter = ref.watch(currentPlayingChapterProvider);
final vanishingPercentage = 1 - percentageMiniplayer;
// final progress =
// useStream(player.slowPositionStreamInBook, initialData: Duration.zero);
final progress =
useStream(player.positionStream, initialData: Duration.zero);
// final vanishingPercentage = 1 - percentageMiniplayer;
// // final progress =
// // useStream(player.slowPositionStreamInBook, initialData: Duration.zero);
// final progress =
// 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(
alignment: Alignment.topCenter,
children: [
Row(
children: [
// image
Padding(
padding: EdgeInsets.only(
left: ((availWidth - maxImgSize) / 2) * percentageMiniplayer,
),
child: InkWell(
onTap: () {
// navigate to item page
context.pushNamed(
Routes.libraryItem.name,
pathParameters: {
Routes.libraryItem.pathParamName!:
player.book!.libraryItemId,
},
);
},
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxImgSize,
),
child: imgWidget,
),
),
),
// author and title of the book
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8),
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),
),
),
],
),
),
),
// IconButton(
// icon: const Icon(Icons.fullscreen),
// onPressed: () {
// controller.animateToHeight(state: PanelState.MAX);
// },
// ),
// return Stack(
// alignment: Alignment.topCenter,
// children: [
// Row(
// children: [
// // image
// Padding(
// padding: EdgeInsets.only(
// left: ((availWidth - maxImgSize) / 2) * percentageMiniplayer,
// ),
// child: InkWell(
// onTap: () {
// // navigate to item page
// context.pushNamed(
// Routes.libraryItem.name,
// pathParameters: {
// Routes.libraryItem.pathParamName!:
// player.book!.libraryItemId,
// },
// );
// },
// child: ConstrainedBox(
// constraints: BoxConstraints(
// maxWidth: maxImgSize,
// ),
// child: imgWidget,
// ),
// ),
// ),
// // author and title of the book
// Expanded(
// child: Padding(
// padding: const EdgeInsets.only(left: 8),
// 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),
// ),
// ),
// ],
// ),
// ),
// ),
// // IconButton(
// // icon: const Icon(Icons.fullscreen),
// // onPressed: () {
// // controller.animateToHeight(state: PanelState.MAX);
// // },
// // ),
// rewind button
Opacity(
opacity: vanishingPercentage,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
icon: const Icon(
Icons.replay_30,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {},
),
),
),
// // rewind button
// Opacity(
// opacity: vanishingPercentage,
// child: Padding(
// padding: const EdgeInsets.only(left: 8),
// child: IconButton(
// icon: const Icon(
// Icons.replay_30,
// size: AppElementSizes.iconSizeSmall,
// ),
// onPressed: () {},
// ),
// ),
// ),
// play/pause button
Opacity(
opacity: vanishingPercentage,
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController,
),
),
),
],
),
SizedBox(
height: 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,
),
),
],
);
}
}
// // play/pause button
// Opacity(
// opacity: vanishingPercentage,
// child: Padding(
// padding: const EdgeInsets.only(right: 8),
// child: AudiobookPlayerPlayPauseButton(
// playPauseController: playPauseController,
// ),
// ),
// ),
// ],
// ),
// SizedBox(
// height: 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

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

View file

@ -5,7 +5,7 @@ import 'package:vaani/features/player/providers/audiobook_player.dart'
show audiobookPlayerProvider;
import 'package:vaani/features/player/providers/currently_playing_provider.dart'
show currentPlayingChapterProvider, currentlyPlayingBookProvider;
import 'package:vaani/features/player/view/player_when_expanded.dart'
import 'package:vaani/features/player/view/player_expanded.dart'
show pendingPlayerModals;
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
import 'package:vaani/main.dart' show appLogger;
@ -117,7 +117,8 @@ class ChapterSelectionModal extends HookConsumerWidget {
key: isCurrent ? chapterKey : null,
onTap: () {
Navigator.of(context).pop();
notifier.seekInBook(chapter.start + 90.ms);
// notifier.seekInBook(chapter.start + 90.ms);
notifier.skipToChapter(chapter.id);
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:vaani/features/per_book_settings/providers/book_settings_provider.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/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:vaani/features/per_book_settings/providers/book_settings_provider.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';
class SkipChapterStartEndButton extends HookConsumerWidget {

View file

@ -1,18 +1,28 @@
import 'dart:async';
import 'package:shelfsdk/audiobookshelf_api.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 {
final Duration start;
final Duration end;
final AudiobookPlayer player;
// id
int? chapterId;
// int _index;
final List<StreamSubscription> _subscriptions = [];
final throttler = Throttler(delay: Duration(seconds: 3));
// final StreamController<PlaybackEvent> _playbackController =
// 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()) {
// _subscriptions.add(
// 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(
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();
player.positionStream.listen((position) {
final chapter = player.currentChapter;
if (chapter == null) {
return;
}
if (chapter.id == chapterId) {
if (end > Duration.zero &&
chapter.duration - (player.positionInBook - chapter.start) <
end) {
throttler.call(() {
Future.microtask(() => skipEnd(chapter));
});
});
}
}
if (chapter.id != chapterId) {
if (start > Duration.zero &&
player.positionInBook - chapter.start < Duration(seconds: 1)) {
throttler.call(() {
Future.microtask(() => skipStart(chapter));
});
}
chapterId = chapter.id;
}
}),
);
}
}
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
void dispose() {
for (var sub in _subscriptions) {
@ -53,38 +119,3 @@ class SkipStartEnd {
// _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';
@Riverpod(keepAlive: true)
@riverpod
class SkipStartEnd extends _$SkipStartEnd {
@override
core.SkipStartEnd? build() {
final player = ref.watch(audiobookPlayerProvider);
final bookId = player.book?.libraryItemId ?? '_';
final player = ref.watch(simpleAudiobookPlayerProvider);
final book = ref.watch(audiobookPlayerProvider.select((v) => v.book));
final bookId = book?.libraryItemId ?? '_';
if (bookId == '_') {
return null;
}
@ -18,6 +19,13 @@ class SkipStartEnd extends _$SkipStartEnd {
final start = bookSettings.playerSettings.skipChapterStart;
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
// **************************************************************************
String _$skipStartEndHash() => r'202cfb36fdb3d3fa12debfb188f87650473a88a9';
String _$skipStartEndHash() => r'857b448eac9bb9ab85cea9217775712e660bc990';
/// See also [SkipStartEnd].
@ProviderFor(SkipStartEnd)
final skipStartEndProvider =
NotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
AutoDisposeNotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
SkipStartEnd.new,
name: r'skipStartEndProvider',
debugGetCreateSourceHash:
@ -20,6 +20,6 @@ final skipStartEndProvider =
allTransitiveDependencies: null,
);
typedef _$SkipStartEnd = Notifier<core.SkipStartEnd?>;
typedef _$SkipStartEnd = AutoDisposeNotifier<core.SkipStartEnd?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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_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/sleep_timer/core/sleep_timer.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),
child: sleepTimer == null
? Icon(
Symbols.bedtime,
Icons.bedtime_outlined,
color: Theme.of(context).colorScheme.onSurface,
)
: RemainingSleepTimeDisplay(
@ -153,7 +152,7 @@ class SleepTimerBottomSheet extends HookConsumerWidget {
onDurationSelected?.call(null);
Navigator.of(context).pop();
},
icon: const Icon(Symbols.bedtime_off),
icon: const Icon(Icons.bedtime_off_outlined),
label: const Text('Cancel Sleep Timer'),
),
),