mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-18 07:19:34 +00:00
替换miniPlayer
This commit is contained in:
parent
eb9b8f3b94
commit
e67d045da6
34 changed files with 1777 additions and 1078 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
// 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(
|
// _logger.fine(
|
||||||
// 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
|
// 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
|
||||||
// );
|
// );
|
||||||
// return AudioSource.uri(
|
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,
|
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);
|
||||||
|
final index = _book!.tracks.indexOf(track);
|
||||||
|
Duration positionInTrack = globalPosition - track.startOffset;
|
||||||
|
if (positionInTrack <= Duration.zero) {
|
||||||
|
positionInTrack = offset;
|
||||||
|
}
|
||||||
|
// 切换到目标音轨具体位置
|
||||||
|
if (index != currentIndex) {
|
||||||
|
await seek(positionInTrack, index: index);
|
||||||
|
}
|
||||||
|
await seek(positionInTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心功能:跳转到指定章节
|
||||||
|
Future<void> skipToChapter(int chapterId, {Duration? position}) async {
|
||||||
|
if (_book == null) return;
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final tracks = _book!.tracks;
|
await seekInBook(chapter.start + offset);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
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,
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
272
lib/features/player/core/audiobook_player_session.dart
Normal file
272
lib/features/player/core/audiobook_player_session.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
1
lib/features/player/providers/player_providers.dart
Normal file
1
lib/features/player/providers/player_providers.dart
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -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() {}
|
|
||||||
|
|
|
||||||
227
lib/features/player/view/player_expanded.dart
Normal file
227
lib/features/player/view/player_expanded.dart
Normal 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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
160
lib/features/player/view/player_minimized.dart
Normal file
160
lib/features/player/view/player_minimized.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
// },
|
// // },
|
||||||
// ),
|
// // ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isForward) {
|
if (isForward) {
|
||||||
player.seekForward();
|
player.seekToNext();
|
||||||
} else {
|
} else {
|
||||||
player.seekBackward();
|
player.seekToPrevious();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
82
lib/features/player/view/widgets/player_progress_bar.dart
Normal file
82
lib/features/player/view/widgets/player_progress_bar.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
if (chapter.id == chapterId) {
|
||||||
|
if (end > Duration.zero &&
|
||||||
|
chapter.duration - (player.positionInBook - chapter.start) <
|
||||||
|
end) {
|
||||||
throttler.call(() {
|
throttler.call(() {
|
||||||
print('跳过片尾');
|
Future.microtask(() => skipEnd(chapter));
|
||||||
Future.microtask(() async {
|
|
||||||
await player.stop();
|
|
||||||
player.seekToNext();
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
);
|
|
||||||
|
|
||||||
// close miniplayer if it is open
|
|
||||||
if (isPlayerExpanded && pendingPlayerModals == 0) {
|
|
||||||
appLogger.fine(
|
|
||||||
'BackButtonListener: closing the player',
|
|
||||||
);
|
|
||||||
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(
|
body: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
children: [
|
children: [
|
||||||
isMobile || isVertical
|
Utils.isMobile() || isVertical
|
||||||
? navigationShell
|
? navigationShell
|
||||||
: buildNavLeft(context, ref),
|
: buildNavLeft(context, ref),
|
||||||
const AudiobookPlayer(),
|
// const AudiobookPlayer(),
|
||||||
|
const PlayerMinimized(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar:
|
bottomNavigationBar:
|
||||||
isMobile || isVertical ? buildNavBottom(context, ref) : null,
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
37
lib/shared/utils/throttler.dart
Normal file
37
lib/shared/utils/throttler.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
pubspec.lock
49
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
16
pubspec.yaml
16
pubspec.yaml
|
|
@ -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
|
||||||
|
|
@ -69,20 +69,20 @@ dependencies:
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue