player seek and chapter change

This commit is contained in:
Dr-Blank 2024-05-19 08:53:21 -04:00
parent 01b3dead49
commit d01855c218
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
17 changed files with 1721 additions and 305 deletions

View file

@ -12,6 +12,7 @@
"audioplayers",
"Autovalidate",
"fullscreen",
"Lerp",
"miniplayer",
"mocktail",
"riverpod",

15
lib/constants/sizes.dart Normal file
View file

@ -0,0 +1,15 @@
class AppElementSizes {
// paddings
static const double paddingRegular = 8.0;
static const double paddingSmall = paddingRegular / 2;
static const double paddingLarge = paddingRegular * 2;
// border radius
static const double borderRadiusRegular = 12.0;
static const double borderRadiusSmall = borderRadiusRegular / 2;
// icon sizes
static const double iconSizeRegular = 48.0;
static const double iconSizeSmall = 36.0;
static const double iconSizeLarge = 64.0;
}

View file

@ -0,0 +1,29 @@
// a table to track preferences of player for each book
import 'package:isar/isar.dart';
part 'book_prefs.g.dart';
/// stores the preferences of the player for a book
@Collection()
@Name('BookPrefs')
class BookPrefs {
@Id()
int libItemId;
double? speed;
// double? volume;
// Duration? sleepTimer;
// bool? showTotalProgress;
// bool? showChapterProgress;
// bool? useChapterInfo;
BookPrefs({
required this.libItemId,
this.speed,
// this.volume,
// this.sleepTimer,
// this.showTotalProgress,
// this.showChapterProgress,
// this.useChapterInfo,
});
}

View file

@ -0,0 +1,496 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'book_prefs.dart';
// **************************************************************************
// _IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors
// ignore_for_file: type=lint
extension GetBookPrefsCollection on Isar {
IsarCollection<int, BookPrefs> get bookPrefs => this.collection();
}
const BookPrefsSchema = IsarGeneratedSchema(
schema: IsarSchema(
name: 'BookPrefs',
idName: 'libItemId',
embedded: false,
properties: [
IsarPropertySchema(
name: 'speed',
type: IsarType.double,
),
],
indexes: [],
),
converter: IsarObjectConverter<int, BookPrefs>(
serialize: serializeBookPrefs,
deserialize: deserializeBookPrefs,
deserializeProperty: deserializeBookPrefsProp,
),
embeddedSchemas: [],
);
@isarProtected
int serializeBookPrefs(IsarWriter writer, BookPrefs object) {
IsarCore.writeDouble(writer, 1, object.speed ?? double.nan);
return object.libItemId;
}
@isarProtected
BookPrefs deserializeBookPrefs(IsarReader reader) {
final int _libItemId;
_libItemId = IsarCore.readId(reader);
final double? _speed;
{
final value = IsarCore.readDouble(reader, 1);
if (value.isNaN) {
_speed = null;
} else {
_speed = value;
}
}
final object = BookPrefs(
libItemId: _libItemId,
speed: _speed,
);
return object;
}
@isarProtected
dynamic deserializeBookPrefsProp(IsarReader reader, int property) {
switch (property) {
case 0:
return IsarCore.readId(reader);
case 1:
{
final value = IsarCore.readDouble(reader, 1);
if (value.isNaN) {
return null;
} else {
return value;
}
}
default:
throw ArgumentError('Unknown property: $property');
}
}
sealed class _BookPrefsUpdate {
bool call({
required int libItemId,
double? speed,
});
}
class _BookPrefsUpdateImpl implements _BookPrefsUpdate {
const _BookPrefsUpdateImpl(this.collection);
final IsarCollection<int, BookPrefs> collection;
@override
bool call({
required int libItemId,
Object? speed = ignore,
}) {
return collection.updateProperties([
libItemId
], {
if (speed != ignore) 1: speed as double?,
}) >
0;
}
}
sealed class _BookPrefsUpdateAll {
int call({
required List<int> libItemId,
double? speed,
});
}
class _BookPrefsUpdateAllImpl implements _BookPrefsUpdateAll {
const _BookPrefsUpdateAllImpl(this.collection);
final IsarCollection<int, BookPrefs> collection;
@override
int call({
required List<int> libItemId,
Object? speed = ignore,
}) {
return collection.updateProperties(libItemId, {
if (speed != ignore) 1: speed as double?,
});
}
}
extension BookPrefsUpdate on IsarCollection<int, BookPrefs> {
_BookPrefsUpdate get update => _BookPrefsUpdateImpl(this);
_BookPrefsUpdateAll get updateAll => _BookPrefsUpdateAllImpl(this);
}
sealed class _BookPrefsQueryUpdate {
int call({
double? speed,
});
}
class _BookPrefsQueryUpdateImpl implements _BookPrefsQueryUpdate {
const _BookPrefsQueryUpdateImpl(this.query, {this.limit});
final IsarQuery<BookPrefs> query;
final int? limit;
@override
int call({
Object? speed = ignore,
}) {
return query.updateProperties(limit: limit, {
if (speed != ignore) 1: speed as double?,
});
}
}
extension BookPrefsQueryUpdate on IsarQuery<BookPrefs> {
_BookPrefsQueryUpdate get updateFirst =>
_BookPrefsQueryUpdateImpl(this, limit: 1);
_BookPrefsQueryUpdate get updateAll => _BookPrefsQueryUpdateImpl(this);
}
class _BookPrefsQueryBuilderUpdateImpl implements _BookPrefsQueryUpdate {
const _BookPrefsQueryBuilderUpdateImpl(this.query, {this.limit});
final QueryBuilder<BookPrefs, BookPrefs, QOperations> query;
final int? limit;
@override
int call({
Object? speed = ignore,
}) {
final q = query.build();
try {
return q.updateProperties(limit: limit, {
if (speed != ignore) 1: speed as double?,
});
} finally {
q.close();
}
}
}
extension BookPrefsQueryBuilderUpdate
on QueryBuilder<BookPrefs, BookPrefs, QOperations> {
_BookPrefsQueryUpdate get updateFirst =>
_BookPrefsQueryBuilderUpdateImpl(this, limit: 1);
_BookPrefsQueryUpdate get updateAll => _BookPrefsQueryBuilderUpdateImpl(this);
}
extension BookPrefsQueryFilter
on QueryBuilder<BookPrefs, BookPrefs, QFilterCondition> {
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> libItemIdEqualTo(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
EqualCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
libItemIdGreaterThan(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
libItemIdGreaterThanOrEqualTo(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterOrEqualCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> libItemIdLessThan(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
libItemIdLessThanOrEqualTo(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessOrEqualCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> libItemIdBetween(
int lower,
int upper,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
BetweenCondition(
property: 0,
lower: lower,
upper: upper,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const IsNullCondition(property: 1));
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedIsNotNull() {
return QueryBuilder.apply(not(), (query) {
return query.addFilterCondition(const IsNullCondition(property: 1));
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedEqualTo(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
EqualCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedGreaterThan(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
speedGreaterThanOrEqualTo(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterOrEqualCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedLessThan(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
speedLessThanOrEqualTo(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessOrEqualCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedBetween(
double? lower,
double? upper, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
BetweenCondition(
property: 1,
lower: lower,
upper: upper,
epsilon: epsilon,
),
);
});
}
}
extension BookPrefsQueryObject
on QueryBuilder<BookPrefs, BookPrefs, QFilterCondition> {}
extension BookPrefsQuerySortBy on QueryBuilder<BookPrefs, BookPrefs, QSortBy> {
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortByLibItemId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortByLibItemIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0, sort: Sort.desc);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortBySpeed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortBySpeedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1, sort: Sort.desc);
});
}
}
extension BookPrefsQuerySortThenBy
on QueryBuilder<BookPrefs, BookPrefs, QSortThenBy> {
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenByLibItemId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenByLibItemIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0, sort: Sort.desc);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenBySpeed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenBySpeedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1, sort: Sort.desc);
});
}
}
extension BookPrefsQueryWhereDistinct
on QueryBuilder<BookPrefs, BookPrefs, QDistinct> {
QueryBuilder<BookPrefs, BookPrefs, QAfterDistinct> distinctBySpeed() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(1);
});
}
}
extension BookPrefsQueryProperty1
on QueryBuilder<BookPrefs, BookPrefs, QProperty> {
QueryBuilder<BookPrefs, int, QAfterProperty> libItemIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(0);
});
}
QueryBuilder<BookPrefs, double?, QAfterProperty> speedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(1);
});
}
}
extension BookPrefsQueryProperty2<R>
on QueryBuilder<BookPrefs, R, QAfterProperty> {
QueryBuilder<BookPrefs, (R, int), QAfterProperty> libItemIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(0);
});
}
QueryBuilder<BookPrefs, (R, double?), QAfterProperty> speedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(1);
});
}
}
extension BookPrefsQueryProperty3<R1, R2>
on QueryBuilder<BookPrefs, (R1, R2), QAfterProperty> {
QueryBuilder<BookPrefs, (R1, R2, int), QOperations> libItemIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(0);
});
}
QueryBuilder<BookPrefs, (R1, R2, double?), QOperations> speedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(1);
});
}
}

View file

@ -8,6 +8,30 @@ import 'package:just_audio/just_audio.dart';
import 'package:just_audio_background/just_audio_background.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
/// returns the sum of the duration of all the previous tracks before the [index]
Duration sumOfTracks(BookExpanded book, int? index) {
// return 0 if index is less than 0
if (index == null || index < 0) {
return Duration.zero;
}
return book.tracks.sublist(0, index).fold<Duration>(Duration.zero,
(previousValue, element) {
return previousValue + element.duration;
});
}
/// returns the [AudioTrack] to play based on the [position] in the [book]
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
var totalDuration = Duration.zero;
for (var track in book.tracks) {
totalDuration += track.duration;
if (totalDuration >= position) {
return track;
}
}
return book.tracks.last;
}
/// will manage the audio player instance
class AudiobookPlayer extends AudioPlayer {
// constructor which takes in the BookExpanded object
@ -18,6 +42,9 @@ class AudiobookPlayer extends AudioPlayer {
/// the [BookExpanded] being played
BookExpanded? _book;
// /// the [BookExpanded] trying to be played
// BookExpanded? _intended_book;
/// the [BookExpanded] being played
///
/// to set the book, use [setSourceAudioBook]
@ -36,7 +63,12 @@ class AudiobookPlayer extends AudioPlayer {
int? get availableTracks => _book?.tracks.length;
/// sets the current [AudioTrack] as the source of the player
Future<void> setSourceAudioBook(BookExpanded? book) async {
Future<void> setSourceAudioBook(
BookExpanded? book, {
bool preload = true,
// int? initialIndex,
Duration? initialPosition,
}) async {
// if the book is null, stop the player
if (book == null) {
_book = null;
@ -51,7 +83,24 @@ class AudiobookPlayer extends AudioPlayer {
// first stop the player and clear the source
await stop();
_book = book;
// some calculations to set the initial index and position
// initialPosition is of the entire book not just the current track
// hence first we need to calculate the current track which will be used to set the initial position
// then we set the initial index to the current track index and position as the remaining duration from the position
// after subtracting the duration of all the previous tracks
final trackToPlay = getTrackToPlay(book, initialPosition ?? Duration.zero);
final initialIndex = book.tracks.indexOf(trackToPlay);
final initialPositionInTrack = initialPosition != null
? initialPosition - sumOfTracks(book, initialIndex - 1)
: null;
await setAudioSource(
preload: preload,
initialIndex: initialIndex,
initialPosition: initialPositionInTrack,
ConcatenatingAudioSource(
useLazyPreparation: true,
children: book.tracks.map((track) {
@ -73,8 +122,6 @@ class AudiobookPlayer extends AudioPlayer {
).catchError((error) {
debugPrint('Error: $error');
});
_book = book;
}
/// toggles the player between play and pause
@ -84,7 +131,7 @@ class AudiobookPlayer extends AudioPlayer {
throw StateError('No book is set');
}
// ! refactor this
// TODO refactor this
return switch (playerState) {
PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(),
};
@ -93,27 +140,58 @@ class AudiobookPlayer extends AudioPlayer {
/// need to override getDuration and getCurrentPosition to return according to the book instead of the current track
/// this is because the book can be a list of audio files and the player is only aware of the current track
/// so we need to calculate the duration and current position based on the book
// @override
// Future<Duration?> getDuration() async {
// if (_book == null) {
// return null;
// }
// return _book!.tracks.fold<Duration>(
// Duration.zero,
// (previousValue, element) => previousValue + element.duration,
// );
// }
// @override
// Future<Duration?> getCurrentPosition() async {
// if (_book == null) {
// return null;
// }
// var currentTrack = _book!.tracks[_currentIndex];
// var currentTrackDuration = currentTrack.duration;
// var currentTrackPosition = await super.getCurrentPosition();
// return currentTrackPosition != null
// ? currentTrackPosition + currentTrackDuration
// : null;
// }
@override
Future<void> seek(Duration? position, {int? index}) async {
if (_book == null) {
return;
}
if (position == null) {
return;
}
final trackToPlay = getTrackToPlay(_book!, position);
final index = _book!.tracks.indexOf(trackToPlay);
final positionInTrack = position - sumOfTracks(_book!, index - 1);
return super.seek(positionInTrack, index: index);
}
/// streams to override to suit the book instead of the current track
// - positionStream
// - bufferedPositionStream
@override
Stream<Duration> get positionStream {
return super.positionStream.map((position) {
if (_book == null) {
return Duration.zero;
}
return position + sumOfTracks(_book!, sequenceState!.currentIndex);
});
}
@override
Stream<Duration> get bufferedPositionStream {
return super.bufferedPositionStream.map((position) {
if (_book == null) {
return Duration.zero;
}
return position + sumOfTracks(_book!, sequenceState!.currentIndex);
});
}
/// get current chapter
BookChapter? get currentChapter {
if (_book == null) {
return null;
}
return _book!.chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position;
},
orElse: () => _book!.chapters.first,
);
}
}

View file

@ -9,3 +9,30 @@ BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) {
final player = ref.watch(audiobookPlayerProvider);
return player.book;
}
/// provided the current chapter of the book being played
@riverpod
BookChapter? currentPlayingChapter(CurrentPlayingChapterRef ref) {
final player = ref.watch(audiobookPlayerProvider);
// get the current timestamp
final currentTimestamp = player.position;
// get the chapter that contains the current timestamp
return player.book?.chapters.firstWhere(
(element) =>
element.start <= currentTimestamp && element.end >= currentTimestamp,
);
}
/// provides the book metadata of the currently playing book
@riverpod
BookMetadataExpanded? currentBookMetadata(CurrentBookMetadataRef ref) {
final player = ref.watch(audiobookPlayerProvider);
if (player.book == null) return null;
return BookMetadataExpanded.fromJson(player.book!.metadata.toJson());
}
// /// volume of the player [0, 1]
// @riverpod
// double currentVolume(CurrentVolumeRef ref) {
// return 1;
// }

View file

@ -23,5 +23,43 @@ final currentlyPlayingBookProvider =
);
typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef<BookExpanded?>;
String _$currentPlayingChapterHash() =>
r'562416b7e0068aaba9138cb8e0ed7a5ddba8e6c6';
/// provided the current chapter of the book being played
///
/// Copied from [currentPlayingChapter].
@ProviderFor(currentPlayingChapter)
final currentPlayingChapterProvider =
AutoDisposeProvider<BookChapter?>.internal(
currentPlayingChapter,
name: r'currentPlayingChapterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentPlayingChapterHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CurrentPlayingChapterRef = AutoDisposeProviderRef<BookChapter?>;
String _$currentBookMetadataHash() =>
r'02b462a051fce5bcbdad6fdb708b60256fbb588c';
/// provides the book metadata of the currently playing book
///
/// Copied from [currentBookMetadata].
@ProviderFor(currentBookMetadata)
final currentBookMetadataProvider =
AutoDisposeProvider<BookMetadataExpanded?>.internal(
currentBookMetadata,
name: r'currentBookMetadataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentBookMetadataHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CurrentBookMetadataRef = AutoDisposeProviderRef<BookMetadataExpanded?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -10,7 +10,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'player_form.g.dart';
const double playerMinHeight = 70;
const miniplayerPercentageDeclaration = 0.2;
// const miniplayerPercentageDeclaration = 0.2;
extension on Ref {
// We can move the previous logic to a Ref extension.

View file

@ -1,4 +1,5 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -9,31 +10,20 @@ import 'package:whispering_pages/api/library_item_provider.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart';
import 'package:whispering_pages/features/player/providers/player_form.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/shared/extensions/inverse_lerp.dart';
import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart';
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
import 'player_when_expanded.dart';
import 'player_when_minimized.dart';
double valueFromPercentageInRange({
required final double min,
max,
percentage,
}) {
return percentage * (max - min) + min;
}
double percentageFromValueInRange({required final double min, max, value}) {
return (value - min) / (max - min);
}
class AudiobookPlayer extends HookConsumerWidget {
const AudiobookPlayer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final currentBook = ref.watch(currentlyPlayingBookProvider);
if (currentBook == null) {
return const SizedBox.shrink();
@ -67,11 +57,8 @@ class AudiobookPlayer extends HookConsumerWidget {
}
});
final playPauseButton = AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController,
);
const progressBar = AudiobookTotalProgressBar();
const chapterProgressBar = AudiobookChapterProgressBar();
// theme from image
final imageTheme = ref.watch(
@ -84,14 +71,25 @@ class AudiobookPlayer extends HookConsumerWidget {
// 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 maxImgSize = availWidth * 0.9;
final preferredVolume = appSettings.playerSettings.preferredVolume;
return Theme(
data: ThemeData(
colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
),
child: Miniplayer(
valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
onDragDown: (percentage) async {
// preferred volume
// set volume to 0 when dragging down
await player.setVolume(preferredVolume * (1 - percentage));
},
minHeight: playerMinHeight,
// subtract the height of notches and other system UI
maxHeight: playerMaxHeight,
controller: ref.watch(miniplayerControllerProvider),
elevation: 4,
@ -102,81 +100,47 @@ class AudiobookPlayer extends HookConsumerWidget {
builder: (height, percentage) {
// return SafeArea(
// child: Text(
// 'percentage: ${percentage.toStringAsFixed(2)}, height: ${height.toStringAsFixed(2)}',
// 'percentage: ${percentage.toStringAsFixed(2)}, height: ${height.toStringAsFixed(2)} volume: ${player.volume.toStringAsFixed(2)}',
// ),
// );
// at what point should the player switch from miniplayer to expanded player
// at this point the image should be at its max size and in the center of the player
final miniplayerPercentageDeclaration =
(maxImgSize - playerMinHeight) /
(playerMaxHeight - playerMinHeight);
final bool isFormMiniplayer =
percentage < miniplayerPercentageDeclaration;
final double availWidth = MediaQuery.of(context).size.width;
final maxImgSize = availWidth * 0.4;
final bookTitle = Text(player.book?.metadata.title ?? '');
//Declare additional widgets (eg. SkipButton) and variables
if (!isFormMiniplayer) {
var percentageExpandedPlayer = percentageFromValueInRange(
min: playerMaxHeight * miniplayerPercentageDeclaration +
playerMinHeight,
max: playerMaxHeight,
value: height,
);
if (percentageExpandedPlayer < 0) percentageExpandedPlayer = 0;
final paddingVertical = valueFromPercentageInRange(
min: 0,
max: 16,
percentage: percentageExpandedPlayer,
);
final double heightWithoutPadding = height - paddingVertical * 2;
final double imageSize = heightWithoutPadding > maxImgSize
? maxImgSize
: heightWithoutPadding;
final paddingLeft = valueFromPercentageInRange(
min: 0,
max: availWidth - imageSize,
percentage: percentageExpandedPlayer,
) /
2;
// this calculation needs a refactor
var percentageExpandedPlayer = percentage
.inverseLerp(
miniplayerPercentageDeclaration,
1,
)
.clamp(0.0, 1.0);
const buttonSkipForward = IconButton(
icon: Icon(Icons.forward_30),
iconSize: 33,
onPressed: onTap,
);
const buttonSkipBackwards = IconButton(
icon: Icon(Icons.replay_10),
iconSize: 33,
onPressed: onTap,
);
return PlayerWhenExpanded(
imgPaddingLeft: paddingLeft,
imgPaddingVertical: paddingVertical,
imageSize: imageSize,
imageSize: maxImgSize,
img: imgWidget,
percentageExpandedPlayer: percentageExpandedPlayer,
text: bookTitle,
buttonSkipBackwards: buttonSkipBackwards,
playPauseButton: playPauseButton,
buttonSkipForward: buttonSkipForward,
progressIndicator: progressBar,
playPauseController: playPauseController,
);
}
//Miniplayer
final percentageMiniplayer = percentageFromValueInRange(
min: playerMinHeight,
max: playerMaxHeight * miniplayerPercentageDeclaration +
playerMinHeight,
value: height,
final percentageMiniplayer = percentage.inverseLerp(
0,
miniplayerPercentageDeclaration,
);
final elementOpacity = 1 - 1 * percentageMiniplayer;
return PlayerWhenMinimized(
maxImgSize: maxImgSize,
availWidth: availWidth,
imgWidget: imgWidget,
elementOpacity: elementOpacity,
playPauseButton: playPauseButton,
progressIndicator: progressBar,
playPauseController: playPauseController,
percentageMiniplayer: percentageMiniplayer,
);
},
),
@ -188,32 +152,37 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({
super.key,
required this.playPauseController,
this.iconSize = 48.0,
});
final double iconSize;
final AnimationController playPauseController;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
return switch (player.processingState) {
ProcessingState.loading ||
ProcessingState.buffering =>
const CircularProgressIndicator(),
ProcessingState.loading || ProcessingState.buffering => const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
ProcessingState.completed => IconButton(
onPressed: () async {
await player.seek(const Duration(seconds: 0));
await player.play();
},
icon: const Icon(Icons.replay),
icon: const Icon(
Icons.replay,
),
),
ProcessingState.ready => IconButton(
onPressed: () async {
await player.togglePlayPause();
},
iconSize: iconSize,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: playPauseController,
size: 50,
),
),
ProcessingState.idle => const SizedBox.shrink(),
@ -227,56 +196,130 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
class AudiobookTotalProgressBar extends HookConsumerWidget {
const AudiobookTotalProgressBar({
super.key,
this.barHeight = 5.0,
this.barCapShape = BarCapShape.round,
this.thumbRadius = 10.0,
this.thumbGlowRadius = 30.0,
this.thumbCanPaintOutsideBar = true,
this.timeLabelLocation,
this.timeLabelType,
this.timeLabelTextStyle,
this.timeLabelPadding = 0.0,
});
final double barHeight;
final BarCapShape barCapShape;
final double thumbRadius;
final double thumbGlowRadius;
final bool thumbCanPaintOutsideBar;
final TimeLabelLocation? timeLabelLocation;
final TimeLabelType? timeLabelType;
final TextStyle? timeLabelTextStyle;
final double timeLabelPadding;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final position = useStream(
player.positionStream,
initialData: const Duration(seconds: 0),
);
final buffered = useStream(
player.bufferedPositionStream,
initialData: const Duration(seconds: 0),
);
final currentIndex = useStream(
player.currentIndexStream,
initialData: 0,
);
var durationOfPreviousTracks =
player.book?.tracks.sublist(0, currentIndex.data).fold(
const Duration(seconds: 0),
(previousValue, element) => previousValue + element.duration,
) ??
const Duration(seconds: 0);
final totalProgress = durationOfPreviousTracks +
(position.data ?? const Duration(seconds: 0));
final totalBuffered = durationOfPreviousTracks +
(buffered.data ?? const Duration(seconds: 0));
return ProgressBar(
progress: totalProgress,
total: player.book?.duration ?? const Duration(seconds: 0),
onSeek: player.seek,
buffered: totalBuffered,
bufferedBarColor: Theme.of(context).colorScheme.secondary,
thumbRadius: thumbRadius,
thumbGlowRadius: thumbGlowRadius,
thumbCanPaintOutsideBar: thumbCanPaintOutsideBar,
barHeight: barHeight,
barCapShape: barCapShape,
timeLabelLocation: timeLabelLocation,
timeLabelType: timeLabelType,
timeLabelTextStyle: timeLabelTextStyle,
timeLabelPadding: timeLabelPadding,
);
}
}
class AudiobookChapterProgressBar extends HookConsumerWidget {
const AudiobookChapterProgressBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
// final playerState = useState(player.processingState);
// add a listener to the player state
// player.processingStateStream.listen((state) {
// playerState.value = state;
// });
return StreamBuilder(
stream: player.currentIndexStream,
builder: (context, currentTrackIndex) {
return StreamBuilder(
stream: player.positionStream,
builder: (context, progress) {
// totalProgress is the sum of the duration of all the tracks before the current track + the current track position
final totalProgress =
player.book?.tracks.sublist(0, currentTrackIndex.data).fold(
final position = useStream(
player.positionStream,
initialData: const Duration(seconds: 0),
);
final buffered = useStream(
player.bufferedPositionStream,
initialData: const Duration(seconds: 0),
);
final currentIndex = useStream(
player.currentIndexStream,
initialData: 0,
);
final durationOfPreviousTracks =
player.book?.tracks.sublist(0, currentIndex.data).fold(
const Duration(seconds: 0),
(previousValue, element) =>
previousValue + element.duration,
(previousValue, element) => previousValue + element.duration,
) ??
const Duration(seconds: 0) +
(progress.data ?? const Duration(seconds: 0));
return StreamBuilder(
stream: player.bufferedPositionStream,
builder: (context, buffered) {
final totalBuffered =
player.book?.tracks.sublist(0, currentTrackIndex.data).fold(
const Duration(seconds: 0),
(previousValue, element) =>
previousValue + element.duration,
) ??
const Duration(seconds: 0) +
const Duration(seconds: 0);
final totalProgress = durationOfPreviousTracks +
(position.data ?? const Duration(seconds: 0));
final totalBuffered = durationOfPreviousTracks +
(buffered.data ?? const Duration(seconds: 0));
// now find the chapter that corresponds to the current time
// and calculate the progress of the current chapter
final currentChapter = player.book?.chapters.firstWhereOrNull(
(element) =>
(element.start <= totalProgress) && (element.end >= totalProgress),
);
final currentChapterProgress =
currentChapter == null ? null : (totalProgress - currentChapter.start);
final currentChapterBuffered =
currentChapter == null ? null : (totalBuffered - currentChapter.start);
return ProgressBar(
progress: totalProgress,
total: player.book?.duration ?? const Duration(seconds: 0),
onSeek: player.seek,
progress: currentChapterProgress ?? totalProgress,
total: currentChapter == null
? player.book?.duration ?? const Duration(seconds: 0)
: currentChapter.end - currentChapter.start,
// ! TODO add onSeek
onSeek: (duration) {
player.seek(
duration + (currentChapter?.start ?? const Duration(seconds: 0)),
);
},
thumbRadius: 8,
buffered: totalBuffered,
buffered: currentChapterBuffered ?? totalBuffered,
bufferedBarColor: Theme.of(context).colorScheme.secondary,
);
},
);
},
);
},
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}

View file

@ -1,79 +1,331 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:miniplayer/miniplayer.dart';
import 'package:whispering_pages/constants/sizes.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart';
import 'package:whispering_pages/features/player/providers/player_form.dart';
import 'package:whispering_pages/features/player/view/audiobook_player.dart';
import 'package:whispering_pages/shared/extensions/inverse_lerp.dart';
class PlayerWhenExpanded extends StatelessWidget {
class PlayerWhenExpanded extends HookConsumerWidget {
const PlayerWhenExpanded({
super.key,
required this.imgPaddingLeft,
required this.imgPaddingVertical,
required this.imageSize,
required this.img,
required this.percentageExpandedPlayer,
required this.text,
required this.buttonSkipBackwards,
required this.playPauseButton,
required this.buttonSkipForward,
required this.progressIndicator,
required this.playPauseController,
});
/// padding values control the position of the image
final double imgPaddingLeft;
final double imgPaddingVertical;
final double imageSize;
final Widget img;
final double percentageExpandedPlayer;
final Text text;
final IconButton buttonSkipBackwards;
final Widget playPauseButton;
final IconButton buttonSkipForward;
final Widget progressIndicator;
final AnimationController playPauseController;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
/// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer]
/// however, some properties need to start later than 0% and end before 100%
const lateStart = 0.4;
const earlyEnd = 1;
final earlyPercentage = percentageExpandedPlayer
.inverseLerp(
lateStart,
earlyEnd,
)
.clamp(0.0, 1.0);
final currentBook = ref.watch(currentlyPlayingBookProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
return Column(
children: [
Align(
alignment: Alignment.centerLeft,
// sized box for system status bar
SizedBox(
height: MediaQuery.of(context).padding.top * earlyPercentage,
),
// a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100 * earlyPercentage,
),
child: Opacity(
opacity: earlyPercentage,
child: Padding(
padding: EdgeInsets.only(
left: imgPaddingLeft,
top: imgPaddingVertical,
// bottom: paddingVertical,
padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// the down arrow
IconButton(
iconSize: 30,
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () {
// minimize the player
ref.read(miniplayerControllerProvider).animateToHeight(
state: PanelState.MIN,
);
},
),
// the pill shaped container
// SizedBox(
// height: 6,
// width: 32,
// child: Container(
// decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.secondary,
// borderRadius: BorderRadius.circular(32),
// ),
// ),
// ),
// the cast button
IconButton(
icon: const Icon(Icons.cast),
onPressed: () {},
),
],
),
),
),
),
// the image
Padding(
padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
child: Align(
alignment: Alignment.center,
// add a shadow to the image elevation hovering effect
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.1),
blurRadius: 32 * earlyPercentage,
spreadRadius: 8 * earlyPercentage,
// offset: Offset(0, 16 * earlyPercentage),
),
],
),
child: SizedBox(
height: imageSize,
child: InkWell(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular * earlyPercentage,
),
child: img,
),
),
),
),
Expanded(
),
),
// the chapter title
currentChapter == null
? const SizedBox()
: Opacity(
opacity: earlyPercentage,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 33),
child: Opacity(
opacity: percentageExpandedPlayer,
child: Column(
padding: EdgeInsets.only(
top: AppElementSizes.paddingRegular * 4 * earlyPercentage,
// horizontal: 16.0,
),
// child: SizedBox(
// same as the image width
// width: imageSize,
child: Text(
currentChapter.title,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// ),
),
),
// the book name and author
Opacity(
opacity: earlyPercentage,
child: Padding(
padding: EdgeInsets.only(
bottom: AppElementSizes.paddingRegular * earlyPercentage,
// horizontal: 16.0,
),
// child: SizedBox(
// same as the image width
// width: imageSize,
child: Text(
[
currentBookMetadata?.title ?? '',
currentBookMetadata?.authorName ?? '',
].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// ),
),
),
const Spacer(),
// the progress bar
Opacity(
opacity: earlyPercentage,
child: SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
top: AppElementSizes.paddingRegular * earlyPercentage,
left: AppElementSizes.paddingRegular * earlyPercentage,
right: AppElementSizes.paddingRegular * earlyPercentage,
),
child: const AudiobookChapterProgressBar(),
),
),
),
const Spacer(),
// the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
Opacity(
opacity: earlyPercentage,
child: SizedBox(
width: imageSize,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// previous chapter
const AudiobookPlayerSeekChapterButton(isForward: false),
// buttonSkipBackwards
const AudiobookPlayerSeekButton(isForward: false),
AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController,
),
// buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true),
// next chapter
const AudiobookPlayerSeekChapterButton(isForward: true),
],
),
),
),
const Spacer(),
// speed control, sleep timer, chapter list, and settings
Opacity(
opacity: earlyPercentage,
child: Padding(
padding: EdgeInsets.only(
bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Flexible(child: text),
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
buttonSkipBackwards,
playPauseButton,
buttonSkipForward,
// speed control
IconButton(
icon: const Icon(Icons.speed),
onPressed: () {},
),
// sleep timer
IconButton(
icon: const Icon(Icons.timer),
onPressed: () {},
),
// chapter list
IconButton(
icon: const Icon(Icons.menu_book_rounded),
onPressed: () {},
),
// settings
IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {},
),
],
),
),
Flexible(child: progressIndicator),
],
),
),
),
),
],
);
}
}
class AudiobookPlayerSeekButton extends HookConsumerWidget {
const AudiobookPlayerSeekButton({
super.key,
required this.isForward,
});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
return IconButton(
icon: Icon(
isForward ? Icons.forward_30 : Icons.replay_30,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {
if (isForward) {
player.seek(player.position + const Duration(seconds: 30));
} else {
player.seek(player.position - const Duration(seconds: 30));
}
},
);
}
}
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton({
super.key,
required this.isForward,
});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
return IconButton(
icon: Icon(
isForward ? Icons.skip_next : Icons.skip_previous,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {
if (player.book == null) {
return;
}
if (isForward) {
player.seek(player.currentChapter!.end);
} else {
// if player position is less than 5 seconds into the chapter, go to the previous chapter
final chapterPosition =
player.position - player.currentChapter!.start;
if (chapterPosition < const Duration(seconds: 5)) {
final index = player.book!.chapters.indexOf(player.currentChapter!);
if (index > 0) {
player.seek(player.book!.chapters[index - 1].start);
}
} else {
player.seek(player.currentChapter!.start);
}
}
},
);
}
}

View file

@ -1,93 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/constants/sizes.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/player/providers/player_form.dart';
import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart';
import 'package:whispering_pages/features/player/view/audiobook_player.dart';
import 'package:whispering_pages/router/router.dart';
class PlayerWhenMinimized extends HookConsumerWidget {
const PlayerWhenMinimized({
super.key,
required this.availWidth,
required this.maxImgSize,
required this.imgWidget,
required this.elementOpacity,
required this.playPauseButton,
required this.progressIndicator,
required this.playPauseController,
required this.percentageMiniplayer,
});
final double availWidth;
final double maxImgSize;
final Widget imgWidget;
final double elementOpacity;
final Widget playPauseButton;
final Widget progressIndicator;
final AnimationController playPauseController;
/// 0 - 1, from minimized to when switched to expanded player
///
/// by the time 1 is reached only image should be visible in the center of the widget
final double percentageMiniplayer;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final controller = ref.watch(miniplayerControllerProvider);
return Column(
final vanishingPercentage = 1 - percentageMiniplayer;
final progress =
useStream(player.positionStream, initialData: Duration.zero);
final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
var barHeight = vanishingPercentage * 3;
return Stack(
alignment: Alignment.bottomCenter,
children: [
Expanded(
child: Row(
Row(
children: [
ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxImgSize),
// image
Padding(
padding: EdgeInsets.symmetric(
horizontal:
((availWidth - maxImgSize) / 2) * percentageMiniplayer,
),
child: InkWell(
onTap: () {
// navigate to item page
context.pushNamed(
Routes.libraryItem.name,
pathParameters: {
Routes.libraryItem.pathParamName!:
player.book!.libraryItemId,
},
);
},
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxImgSize,
),
child: imgWidget,
),
),
),
// author and title of the book
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Opacity(
opacity: elementOpacity,
padding: const EdgeInsets.only(left: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// AutoScrollText(
Text(
player.book?.metadata.title ?? '',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 16),
bookMetaExpanded?.title ?? '',
maxLines: 1, overflow: TextOverflow.ellipsis,
// velocity:
// const Velocity(pixelsPerSecond: Offset(16, 0)),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'audioObject.subtitle',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
bookMetaExpanded?.authorName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context)
.textTheme
.bodyMedium!
.color!
.withOpacity(0.55),
.colorScheme
.onBackground
.withOpacity(0.7),
),
),
],
),
),
),
),
// IconButton(
// icon: const Icon(Icons.fullscreen),
// onPressed: () {
// controller.animateToHeight(state: PanelState.MAX);
// },
// ),
Padding(
padding: const EdgeInsets.all(8),
child: Opacity(
opacity: elementOpacity,
child: playPauseButton,
// rewind button
Opacity(
opacity: vanishingPercentage,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
icon: const Icon(
Icons.replay_30,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {},
),
),
),
// play/pause button
Opacity(
opacity: vanishingPercentage,
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController,
),
),
),
],
),
SizedBox(
height: barHeight,
child: LinearProgressIndicator(
value: (progress.data ?? Duration.zero).inSeconds /
player.book!.duration.inSeconds,
color: Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
),
// SizedBox(
// height: progressIndicatorHeight,
// child: Opacity(
// opacity: elementOpacity,
// child: progressIndicator,
// ),
// ),
],
);
}

View file

@ -25,17 +25,32 @@ class PlayerSettings with _$PlayerSettings {
const factory PlayerSettings({
@Default(MinimizedPlayerSettings())
MinimizedPlayerSettings miniPlayerSettings,
@Default(ExpandedPlayerSettings())
ExpandedPlayerSettings expandedPlayerSettings,
@Default(1) double preferredVolume,
@Default(1) double preferredSpeed,
@Default(Duration(minutes: 15)) Duration sleepTimer,
}) = _PlayerSettings;
factory PlayerSettings.fromJson(Map<String, dynamic> json) =>
_$PlayerSettingsFromJson(json);
}
@freezed
class ExpandedPlayerSettings with _$ExpandedPlayerSettings {
const factory ExpandedPlayerSettings({
@Default(false) bool showTotalProgress,
@Default(true) bool showChapterProgress,
}) = _ExpandedPlayerSettings;
factory ExpandedPlayerSettings.fromJson(Map<String, dynamic> json) =>
_$ExpandedPlayerSettingsFromJson(json);
}
@freezed
class MinimizedPlayerSettings with _$MinimizedPlayerSettings {
const factory MinimizedPlayerSettings({
@Default(false) bool useChapterInfo,
}) = _MiniPlayerSettings;
}) = _MinimizedPlayerSettings;
factory MinimizedPlayerSettings.fromJson(Map<String, dynamic> json) =>
_$MinimizedPlayerSettingsFromJson(json);

View file

@ -224,6 +224,11 @@ PlayerSettings _$PlayerSettingsFromJson(Map<String, dynamic> json) {
mixin _$PlayerSettings {
MinimizedPlayerSettings get miniPlayerSettings =>
throw _privateConstructorUsedError;
ExpandedPlayerSettings get expandedPlayerSettings =>
throw _privateConstructorUsedError;
double get preferredVolume => throw _privateConstructorUsedError;
double get preferredSpeed => throw _privateConstructorUsedError;
Duration get sleepTimer => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -237,9 +242,15 @@ abstract class $PlayerSettingsCopyWith<$Res> {
PlayerSettings value, $Res Function(PlayerSettings) then) =
_$PlayerSettingsCopyWithImpl<$Res, PlayerSettings>;
@useResult
$Res call({MinimizedPlayerSettings miniPlayerSettings});
$Res call(
{MinimizedPlayerSettings miniPlayerSettings,
ExpandedPlayerSettings expandedPlayerSettings,
double preferredVolume,
double preferredSpeed,
Duration sleepTimer});
$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings;
$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings;
}
/// @nodoc
@ -256,12 +267,32 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
@override
$Res call({
Object? miniPlayerSettings = null,
Object? expandedPlayerSettings = null,
Object? preferredVolume = null,
Object? preferredSpeed = null,
Object? sleepTimer = null,
}) {
return _then(_value.copyWith(
miniPlayerSettings: null == miniPlayerSettings
? _value.miniPlayerSettings
: miniPlayerSettings // ignore: cast_nullable_to_non_nullable
as MinimizedPlayerSettings,
expandedPlayerSettings: null == expandedPlayerSettings
? _value.expandedPlayerSettings
: expandedPlayerSettings // ignore: cast_nullable_to_non_nullable
as ExpandedPlayerSettings,
preferredVolume: null == preferredVolume
? _value.preferredVolume
: preferredVolume // ignore: cast_nullable_to_non_nullable
as double,
preferredSpeed: null == preferredSpeed
? _value.preferredSpeed
: preferredSpeed // ignore: cast_nullable_to_non_nullable
as double,
sleepTimer: null == sleepTimer
? _value.sleepTimer
: sleepTimer // ignore: cast_nullable_to_non_nullable
as Duration,
) as $Val);
}
@ -273,6 +304,15 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
return _then(_value.copyWith(miniPlayerSettings: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings {
return $ExpandedPlayerSettingsCopyWith<$Res>(_value.expandedPlayerSettings,
(value) {
return _then(_value.copyWith(expandedPlayerSettings: value) as $Val);
});
}
}
/// @nodoc
@ -283,10 +323,17 @@ abstract class _$$PlayerSettingsImplCopyWith<$Res>
__$$PlayerSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({MinimizedPlayerSettings miniPlayerSettings});
$Res call(
{MinimizedPlayerSettings miniPlayerSettings,
ExpandedPlayerSettings expandedPlayerSettings,
double preferredVolume,
double preferredSpeed,
Duration sleepTimer});
@override
$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings;
@override
$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings;
}
/// @nodoc
@ -301,12 +348,32 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
@override
$Res call({
Object? miniPlayerSettings = null,
Object? expandedPlayerSettings = null,
Object? preferredVolume = null,
Object? preferredSpeed = null,
Object? sleepTimer = null,
}) {
return _then(_$PlayerSettingsImpl(
miniPlayerSettings: null == miniPlayerSettings
? _value.miniPlayerSettings
: miniPlayerSettings // ignore: cast_nullable_to_non_nullable
as MinimizedPlayerSettings,
expandedPlayerSettings: null == expandedPlayerSettings
? _value.expandedPlayerSettings
: expandedPlayerSettings // ignore: cast_nullable_to_non_nullable
as ExpandedPlayerSettings,
preferredVolume: null == preferredVolume
? _value.preferredVolume
: preferredVolume // ignore: cast_nullable_to_non_nullable
as double,
preferredSpeed: null == preferredSpeed
? _value.preferredSpeed
: preferredSpeed // ignore: cast_nullable_to_non_nullable
as double,
sleepTimer: null == sleepTimer
? _value.sleepTimer
: sleepTimer // ignore: cast_nullable_to_non_nullable
as Duration,
));
}
}
@ -315,7 +382,11 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
@JsonSerializable()
class _$PlayerSettingsImpl implements _PlayerSettings {
const _$PlayerSettingsImpl(
{this.miniPlayerSettings = const MinimizedPlayerSettings()});
{this.miniPlayerSettings = const MinimizedPlayerSettings(),
this.expandedPlayerSettings = const ExpandedPlayerSettings(),
this.preferredVolume = 1,
this.preferredSpeed = 1,
this.sleepTimer = const Duration(minutes: 15)});
factory _$PlayerSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$PlayerSettingsImplFromJson(json);
@ -323,10 +394,22 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
@override
@JsonKey()
final MinimizedPlayerSettings miniPlayerSettings;
@override
@JsonKey()
final ExpandedPlayerSettings expandedPlayerSettings;
@override
@JsonKey()
final double preferredVolume;
@override
@JsonKey()
final double preferredSpeed;
@override
@JsonKey()
final Duration sleepTimer;
@override
String toString() {
return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings)';
return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredVolume: $preferredVolume, preferredSpeed: $preferredSpeed, sleepTimer: $sleepTimer)';
}
@override
@ -335,12 +418,21 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
(other.runtimeType == runtimeType &&
other is _$PlayerSettingsImpl &&
(identical(other.miniPlayerSettings, miniPlayerSettings) ||
other.miniPlayerSettings == miniPlayerSettings));
other.miniPlayerSettings == miniPlayerSettings) &&
(identical(other.expandedPlayerSettings, expandedPlayerSettings) ||
other.expandedPlayerSettings == expandedPlayerSettings) &&
(identical(other.preferredVolume, preferredVolume) ||
other.preferredVolume == preferredVolume) &&
(identical(other.preferredSpeed, preferredSpeed) ||
other.preferredSpeed == preferredSpeed) &&
(identical(other.sleepTimer, sleepTimer) ||
other.sleepTimer == sleepTimer));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, miniPlayerSettings);
int get hashCode => Object.hash(runtimeType, miniPlayerSettings,
expandedPlayerSettings, preferredVolume, preferredSpeed, sleepTimer);
@JsonKey(ignore: true)
@override
@ -359,8 +451,11 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
abstract class _PlayerSettings implements PlayerSettings {
const factory _PlayerSettings(
{final MinimizedPlayerSettings miniPlayerSettings}) =
_$PlayerSettingsImpl;
{final MinimizedPlayerSettings miniPlayerSettings,
final ExpandedPlayerSettings expandedPlayerSettings,
final double preferredVolume,
final double preferredSpeed,
final Duration sleepTimer}) = _$PlayerSettingsImpl;
factory _PlayerSettings.fromJson(Map<String, dynamic> json) =
_$PlayerSettingsImpl.fromJson;
@ -368,14 +463,188 @@ abstract class _PlayerSettings implements PlayerSettings {
@override
MinimizedPlayerSettings get miniPlayerSettings;
@override
ExpandedPlayerSettings get expandedPlayerSettings;
@override
double get preferredVolume;
@override
double get preferredSpeed;
@override
Duration get sleepTimer;
@override
@JsonKey(ignore: true)
_$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ExpandedPlayerSettings _$ExpandedPlayerSettingsFromJson(
Map<String, dynamic> json) {
return _ExpandedPlayerSettings.fromJson(json);
}
/// @nodoc
mixin _$ExpandedPlayerSettings {
bool get showTotalProgress => throw _privateConstructorUsedError;
bool get showChapterProgress => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ExpandedPlayerSettingsCopyWith<ExpandedPlayerSettings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ExpandedPlayerSettingsCopyWith<$Res> {
factory $ExpandedPlayerSettingsCopyWith(ExpandedPlayerSettings value,
$Res Function(ExpandedPlayerSettings) then) =
_$ExpandedPlayerSettingsCopyWithImpl<$Res, ExpandedPlayerSettings>;
@useResult
$Res call({bool showTotalProgress, bool showChapterProgress});
}
/// @nodoc
class _$ExpandedPlayerSettingsCopyWithImpl<$Res,
$Val extends ExpandedPlayerSettings>
implements $ExpandedPlayerSettingsCopyWith<$Res> {
_$ExpandedPlayerSettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? showTotalProgress = null,
Object? showChapterProgress = null,
}) {
return _then(_value.copyWith(
showTotalProgress: null == showTotalProgress
? _value.showTotalProgress
: showTotalProgress // ignore: cast_nullable_to_non_nullable
as bool,
showChapterProgress: null == showChapterProgress
? _value.showChapterProgress
: showChapterProgress // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$ExpandedPlayerSettingsImplCopyWith<$Res>
implements $ExpandedPlayerSettingsCopyWith<$Res> {
factory _$$ExpandedPlayerSettingsImplCopyWith(
_$ExpandedPlayerSettingsImpl value,
$Res Function(_$ExpandedPlayerSettingsImpl) then) =
__$$ExpandedPlayerSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool showTotalProgress, bool showChapterProgress});
}
/// @nodoc
class __$$ExpandedPlayerSettingsImplCopyWithImpl<$Res>
extends _$ExpandedPlayerSettingsCopyWithImpl<$Res,
_$ExpandedPlayerSettingsImpl>
implements _$$ExpandedPlayerSettingsImplCopyWith<$Res> {
__$$ExpandedPlayerSettingsImplCopyWithImpl(
_$ExpandedPlayerSettingsImpl _value,
$Res Function(_$ExpandedPlayerSettingsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? showTotalProgress = null,
Object? showChapterProgress = null,
}) {
return _then(_$ExpandedPlayerSettingsImpl(
showTotalProgress: null == showTotalProgress
? _value.showTotalProgress
: showTotalProgress // ignore: cast_nullable_to_non_nullable
as bool,
showChapterProgress: null == showChapterProgress
? _value.showChapterProgress
: showChapterProgress // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ExpandedPlayerSettingsImpl implements _ExpandedPlayerSettings {
const _$ExpandedPlayerSettingsImpl(
{this.showTotalProgress = false, this.showChapterProgress = true});
factory _$ExpandedPlayerSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$ExpandedPlayerSettingsImplFromJson(json);
@override
@JsonKey()
final bool showTotalProgress;
@override
@JsonKey()
final bool showChapterProgress;
@override
String toString() {
return 'ExpandedPlayerSettings(showTotalProgress: $showTotalProgress, showChapterProgress: $showChapterProgress)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ExpandedPlayerSettingsImpl &&
(identical(other.showTotalProgress, showTotalProgress) ||
other.showTotalProgress == showTotalProgress) &&
(identical(other.showChapterProgress, showChapterProgress) ||
other.showChapterProgress == showChapterProgress));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, showTotalProgress, showChapterProgress);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl>
get copyWith => __$$ExpandedPlayerSettingsImplCopyWithImpl<
_$ExpandedPlayerSettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ExpandedPlayerSettingsImplToJson(
this,
);
}
}
abstract class _ExpandedPlayerSettings implements ExpandedPlayerSettings {
const factory _ExpandedPlayerSettings(
{final bool showTotalProgress,
final bool showChapterProgress}) = _$ExpandedPlayerSettingsImpl;
factory _ExpandedPlayerSettings.fromJson(Map<String, dynamic> json) =
_$ExpandedPlayerSettingsImpl.fromJson;
@override
bool get showTotalProgress;
@override
bool get showChapterProgress;
@override
@JsonKey(ignore: true)
_$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl>
get copyWith => throw _privateConstructorUsedError;
}
MinimizedPlayerSettings _$MinimizedPlayerSettingsFromJson(
Map<String, dynamic> json) {
return _MiniPlayerSettings.fromJson(json);
return _MinimizedPlayerSettings.fromJson(json);
}
/// @nodoc
@ -423,23 +692,25 @@ class _$MinimizedPlayerSettingsCopyWithImpl<$Res,
}
/// @nodoc
abstract class _$$MiniPlayerSettingsImplCopyWith<$Res>
abstract class _$$MinimizedPlayerSettingsImplCopyWith<$Res>
implements $MinimizedPlayerSettingsCopyWith<$Res> {
factory _$$MiniPlayerSettingsImplCopyWith(_$MiniPlayerSettingsImpl value,
$Res Function(_$MiniPlayerSettingsImpl) then) =
__$$MiniPlayerSettingsImplCopyWithImpl<$Res>;
factory _$$MinimizedPlayerSettingsImplCopyWith(
_$MinimizedPlayerSettingsImpl value,
$Res Function(_$MinimizedPlayerSettingsImpl) then) =
__$$MinimizedPlayerSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool useChapterInfo});
}
/// @nodoc
class __$$MiniPlayerSettingsImplCopyWithImpl<$Res>
class __$$MinimizedPlayerSettingsImplCopyWithImpl<$Res>
extends _$MinimizedPlayerSettingsCopyWithImpl<$Res,
_$MiniPlayerSettingsImpl>
implements _$$MiniPlayerSettingsImplCopyWith<$Res> {
__$$MiniPlayerSettingsImplCopyWithImpl(_$MiniPlayerSettingsImpl _value,
$Res Function(_$MiniPlayerSettingsImpl) _then)
_$MinimizedPlayerSettingsImpl>
implements _$$MinimizedPlayerSettingsImplCopyWith<$Res> {
__$$MinimizedPlayerSettingsImplCopyWithImpl(
_$MinimizedPlayerSettingsImpl _value,
$Res Function(_$MinimizedPlayerSettingsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@ -447,7 +718,7 @@ class __$$MiniPlayerSettingsImplCopyWithImpl<$Res>
$Res call({
Object? useChapterInfo = null,
}) {
return _then(_$MiniPlayerSettingsImpl(
return _then(_$MinimizedPlayerSettingsImpl(
useChapterInfo: null == useChapterInfo
? _value.useChapterInfo
: useChapterInfo // ignore: cast_nullable_to_non_nullable
@ -458,11 +729,11 @@ class __$$MiniPlayerSettingsImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$MiniPlayerSettingsImpl implements _MiniPlayerSettings {
const _$MiniPlayerSettingsImpl({this.useChapterInfo = false});
class _$MinimizedPlayerSettingsImpl implements _MinimizedPlayerSettings {
const _$MinimizedPlayerSettingsImpl({this.useChapterInfo = false});
factory _$MiniPlayerSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$MiniPlayerSettingsImplFromJson(json);
factory _$MinimizedPlayerSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$MinimizedPlayerSettingsImplFromJson(json);
@override
@JsonKey()
@ -477,7 +748,7 @@ class _$MiniPlayerSettingsImpl implements _MiniPlayerSettings {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MiniPlayerSettingsImpl &&
other is _$MinimizedPlayerSettingsImpl &&
(identical(other.useChapterInfo, useChapterInfo) ||
other.useChapterInfo == useChapterInfo));
}
@ -489,29 +760,29 @@ class _$MiniPlayerSettingsImpl implements _MiniPlayerSettings {
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$MiniPlayerSettingsImplCopyWith<_$MiniPlayerSettingsImpl> get copyWith =>
__$$MiniPlayerSettingsImplCopyWithImpl<_$MiniPlayerSettingsImpl>(
this, _$identity);
_$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl>
get copyWith => __$$MinimizedPlayerSettingsImplCopyWithImpl<
_$MinimizedPlayerSettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$MiniPlayerSettingsImplToJson(
return _$$MinimizedPlayerSettingsImplToJson(
this,
);
}
}
abstract class _MiniPlayerSettings implements MinimizedPlayerSettings {
const factory _MiniPlayerSettings({final bool useChapterInfo}) =
_$MiniPlayerSettingsImpl;
abstract class _MinimizedPlayerSettings implements MinimizedPlayerSettings {
const factory _MinimizedPlayerSettings({final bool useChapterInfo}) =
_$MinimizedPlayerSettingsImpl;
factory _MiniPlayerSettings.fromJson(Map<String, dynamic> json) =
_$MiniPlayerSettingsImpl.fromJson;
factory _MinimizedPlayerSettings.fromJson(Map<String, dynamic> json) =
_$MinimizedPlayerSettingsImpl.fromJson;
@override
bool get useChapterInfo;
@override
@JsonKey(ignore: true)
_$$MiniPlayerSettingsImplCopyWith<_$MiniPlayerSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
_$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl>
get copyWith => throw _privateConstructorUsedError;
}

View file

@ -30,22 +30,49 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map<String, dynamic> json) =>
? const MinimizedPlayerSettings()
: MinimizedPlayerSettings.fromJson(
json['miniPlayerSettings'] as Map<String, dynamic>),
expandedPlayerSettings: json['expandedPlayerSettings'] == null
? const ExpandedPlayerSettings()
: ExpandedPlayerSettings.fromJson(
json['expandedPlayerSettings'] as Map<String, dynamic>),
preferredVolume: (json['preferredVolume'] as num?)?.toDouble() ?? 1,
preferredSpeed: (json['preferredSpeed'] as num?)?.toDouble() ?? 1,
sleepTimer: json['sleepTimer'] == null
? const Duration(minutes: 15)
: Duration(microseconds: (json['sleepTimer'] as num).toInt()),
);
Map<String, dynamic> _$$PlayerSettingsImplToJson(
_$PlayerSettingsImpl instance) =>
<String, dynamic>{
'miniPlayerSettings': instance.miniPlayerSettings,
'expandedPlayerSettings': instance.expandedPlayerSettings,
'preferredVolume': instance.preferredVolume,
'preferredSpeed': instance.preferredSpeed,
'sleepTimer': instance.sleepTimer.inMicroseconds,
};
_$MiniPlayerSettingsImpl _$$MiniPlayerSettingsImplFromJson(
_$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson(
Map<String, dynamic> json) =>
_$MiniPlayerSettingsImpl(
_$ExpandedPlayerSettingsImpl(
showTotalProgress: json['showTotalProgress'] as bool? ?? false,
showChapterProgress: json['showChapterProgress'] as bool? ?? true,
);
Map<String, dynamic> _$$ExpandedPlayerSettingsImplToJson(
_$ExpandedPlayerSettingsImpl instance) =>
<String, dynamic>{
'showTotalProgress': instance.showTotalProgress,
'showChapterProgress': instance.showChapterProgress,
};
_$MinimizedPlayerSettingsImpl _$$MinimizedPlayerSettingsImplFromJson(
Map<String, dynamic> json) =>
_$MinimizedPlayerSettingsImpl(
useChapterInfo: json['useChapterInfo'] as bool? ?? false,
);
Map<String, dynamic> _$$MiniPlayerSettingsImplToJson(
_$MiniPlayerSettingsImpl instance) =>
Map<String, dynamic> _$$MinimizedPlayerSettingsImplToJson(
_$MinimizedPlayerSettingsImpl instance) =>
<String, dynamic>{
'useChapterInfo': instance.useChapterInfo,
};

View file

@ -0,0 +1,13 @@
extension InverseLerp on num {
/// Returns the fraction of this value between [min] and [max].
double inverseLerp(num min, num max) {
return (this - min) / (max - min);
}
}
extension Lerp on double {
/// Returns the value between [min] and [max] given the fraction [t].
double lerp(double min, double max) {
return min + ((max - min) * this);
}
}

View file

@ -361,6 +361,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
file_picker:
dependency: transitive
description:
name: file_picker
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
url: "https://pub.dev"
source: hosted
version: "5.5.0"
fixnum:
dependency: transitive
description:
@ -390,6 +398,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.2"
flutter_colorpicker:
dependency: transitive
description:
name: flutter_colorpicker
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter_hooks:
dependency: "direct main"
description:
@ -406,6 +422,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_material_pickers:
dependency: "direct main"
description:
name: flutter_material_pickers
sha256: "1100bfd9a296a6680578aba8c51a0db114fb8ef94708fe320fe6da92b1f8c0e1"
url: "https://pub.dev"
source: hosted
version: "3.6.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
url: "https://pub.dev"
source: hosted
version: "2.0.19"
flutter_riverpod:
dependency: transitive
description:
@ -552,6 +584,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.7"
infinite_listview:
dependency: transitive
description:
name: infinite_listview
sha256: f6062c1720eb59be553dfa6b89813d3e8dd2f054538445aaa5edaddfa5195ce6
url: "https://pub.dev"
source: hosted
version: "1.1.0"
intl:
dependency: transitive
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
io:
dependency: transitive
description:
@ -751,6 +799,14 @@ packages:
relative: true
source: path
version: "1.0.3"
numberpicker:
dependency: "direct main"
description:
name: numberpicker
sha256: "4c129154944b0f6b133e693f8749c3f8bfb67c4d07ef9dcab48b595c22d1f156"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
octo_image:
dependency: transitive
description:

View file

@ -32,6 +32,7 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
dependencies:
animated_list_plus: ^0.5.2
animated_theme_switcher: ^2.0.10
flutter_material_pickers: ^3.6.0
audio_session: ^0.1.19
audio_video_progress_bar: ^2.0.2
auto_scroll_text: ^0.0.7
@ -62,6 +63,7 @@ dependencies:
media_kit_libs_windows_audio: any
miniplayer:
path: ../miniplayer
numberpicker: ^2.1.2
path: ^1.9.0
path_provider: ^2.1.0
riverpod_annotation: ^2.3.5