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", "audioplayers",
"Autovalidate", "Autovalidate",
"fullscreen", "fullscreen",
"Lerp",
"miniplayer", "miniplayer",
"mocktail", "mocktail",
"riverpod", "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:just_audio_background/just_audio_background.dart';
import 'package:shelfsdk/audiobookshelf_api.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 /// 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
@ -18,6 +42,9 @@ class AudiobookPlayer extends AudioPlayer {
/// the [BookExpanded] being played /// the [BookExpanded] being played
BookExpanded? _book; BookExpanded? _book;
// /// the [BookExpanded] trying to be played
// BookExpanded? _intended_book;
/// the [BookExpanded] being played /// the [BookExpanded] being played
/// ///
/// to set the book, use [setSourceAudioBook] /// to set the book, use [setSourceAudioBook]
@ -36,7 +63,12 @@ class AudiobookPlayer extends AudioPlayer {
int? get availableTracks => _book?.tracks.length; int? get availableTracks => _book?.tracks.length;
/// sets the current [AudioTrack] as the source of the player /// 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 the book is null, stop the player
if (book == null) { if (book == null) {
_book = null; _book = null;
@ -51,7 +83,24 @@ class AudiobookPlayer extends AudioPlayer {
// first stop the player and clear the source // first stop the player and clear the source
await stop(); 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( await setAudioSource(
preload: preload,
initialIndex: initialIndex,
initialPosition: initialPositionInTrack,
ConcatenatingAudioSource( ConcatenatingAudioSource(
useLazyPreparation: true, useLazyPreparation: true,
children: book.tracks.map((track) { children: book.tracks.map((track) {
@ -73,8 +122,6 @@ class AudiobookPlayer extends AudioPlayer {
).catchError((error) { ).catchError((error) {
debugPrint('Error: $error'); debugPrint('Error: $error');
}); });
_book = book;
} }
/// toggles the player between play and pause /// toggles the player between play and pause
@ -84,7 +131,7 @@ class AudiobookPlayer extends AudioPlayer {
throw StateError('No book is set'); throw StateError('No book is set');
} }
// ! refactor this // TODO refactor this
return switch (playerState) { return switch (playerState) {
PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), 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 /// 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
// @override
// Future<Duration?> getDuration() async {
// if (_book == null) {
// return null;
// }
// return _book!.tracks.fold<Duration>(
// Duration.zero,
// (previousValue, element) => previousValue + element.duration,
// );
// }
// @override @override
// Future<Duration?> getCurrentPosition() async { Future<void> seek(Duration? position, {int? index}) async {
// if (_book == null) { if (_book == null) {
// return null; return;
// } }
// var currentTrack = _book!.tracks[_currentIndex]; if (position == null) {
// var currentTrackDuration = currentTrack.duration; return;
// var currentTrackPosition = await super.getCurrentPosition(); }
// return currentTrackPosition != null final trackToPlay = getTrackToPlay(_book!, position);
// ? currentTrackPosition + currentTrackDuration final index = _book!.tracks.indexOf(trackToPlay);
// : null; 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); final player = ref.watch(audiobookPlayerProvider);
return player.book; 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?>; 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: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // 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'; part 'player_form.g.dart';
const double playerMinHeight = 70; const double playerMinHeight = 70;
const miniplayerPercentageDeclaration = 0.2; // const miniplayerPercentageDeclaration = 0.2;
extension on Ref { extension on Ref {
// We can move the previous logic to a Ref extension. // 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:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:collection/collection.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';
@ -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/audiobook_player.dart';
import 'package:whispering_pages/features/player/providers/currently_playing_provider.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/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/shared/widgets/shelves/book_shelf.dart';
import 'package:whispering_pages/theme/theme_from_cover_provider.dart'; import 'package:whispering_pages/theme/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';
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 { 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 currentBook = ref.watch(currentlyPlayingBookProvider); final currentBook = ref.watch(currentlyPlayingBookProvider);
if (currentBook == null) { if (currentBook == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -67,11 +57,8 @@ class AudiobookPlayer extends HookConsumerWidget {
} }
}); });
final playPauseButton = AudiobookPlayerPlayPauseButton(
playPauseController: playPauseController,
);
const progressBar = AudiobookTotalProgressBar(); const progressBar = AudiobookTotalProgressBar();
const chapterProgressBar = AudiobookChapterProgressBar();
// theme from image // theme from image
final imageTheme = ref.watch( final imageTheme = ref.watch(
@ -84,14 +71,25 @@ class AudiobookPlayer extends HookConsumerWidget {
// 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;
// the image width when the player is expanded
final maxImgSize = availWidth * 0.9;
final preferredVolume = appSettings.playerSettings.preferredVolume;
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 {
// preferred volume
// set volume to 0 when dragging down
await player.setVolume(preferredVolume * (1 - percentage));
},
minHeight: playerMinHeight, minHeight: playerMinHeight,
// subtract the height of notches and other system UI
maxHeight: playerMaxHeight, maxHeight: playerMaxHeight,
controller: ref.watch(miniplayerControllerProvider), controller: ref.watch(miniplayerControllerProvider),
elevation: 4, elevation: 4,
@ -102,81 +100,47 @@ class AudiobookPlayer extends HookConsumerWidget {
builder: (height, percentage) { builder: (height, percentage) {
// return SafeArea( // return SafeArea(
// child: Text( // 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 = final bool isFormMiniplayer =
percentage < miniplayerPercentageDeclaration; 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) { if (!isFormMiniplayer) {
var percentageExpandedPlayer = percentageFromValueInRange( // this calculation needs a refactor
min: playerMaxHeight * miniplayerPercentageDeclaration + var percentageExpandedPlayer = percentage
playerMinHeight, .inverseLerp(
max: playerMaxHeight, miniplayerPercentageDeclaration,
value: height, 1,
); )
if (percentageExpandedPlayer < 0) percentageExpandedPlayer = 0; .clamp(0.0, 1.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;
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( return PlayerWhenExpanded(
imgPaddingLeft: paddingLeft, imageSize: maxImgSize,
imgPaddingVertical: paddingVertical,
imageSize: imageSize,
img: imgWidget, img: imgWidget,
percentageExpandedPlayer: percentageExpandedPlayer, percentageExpandedPlayer: percentageExpandedPlayer,
text: bookTitle, playPauseController: playPauseController,
buttonSkipBackwards: buttonSkipBackwards,
playPauseButton: playPauseButton,
buttonSkipForward: buttonSkipForward,
progressIndicator: progressBar,
); );
} }
//Miniplayer //Miniplayer
final percentageMiniplayer = percentageFromValueInRange( final percentageMiniplayer = percentage.inverseLerp(
min: playerMinHeight, 0,
max: playerMaxHeight * miniplayerPercentageDeclaration + miniplayerPercentageDeclaration,
playerMinHeight,
value: height,
); );
final elementOpacity = 1 - 1 * percentageMiniplayer;
return PlayerWhenMinimized( return PlayerWhenMinimized(
maxImgSize: maxImgSize, maxImgSize: maxImgSize,
availWidth: availWidth,
imgWidget: imgWidget, imgWidget: imgWidget,
elementOpacity: elementOpacity, playPauseController: playPauseController,
playPauseButton: playPauseButton, percentageMiniplayer: percentageMiniplayer,
progressIndicator: progressBar,
); );
}, },
), ),
@ -188,32 +152,37 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({ const AudiobookPlayerPlayPauseButton({
super.key, super.key,
required this.playPauseController, required this.playPauseController,
this.iconSize = 48.0,
}); });
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.loading || ProcessingState.buffering => const Padding(
ProcessingState.buffering => padding: EdgeInsets.all(8.0),
const CircularProgressIndicator(), child: CircularProgressIndicator(),
),
ProcessingState.completed => IconButton( ProcessingState.completed => IconButton(
onPressed: () async { onPressed: () async {
await player.seek(const Duration(seconds: 0)); await player.seek(const Duration(seconds: 0));
await player.play(); await player.play();
}, },
icon: const Icon(Icons.replay), icon: const Icon(
Icons.replay,
),
), ),
ProcessingState.ready => IconButton( ProcessingState.ready => IconButton(
onPressed: () async { onPressed: () async {
await player.togglePlayPause(); await player.togglePlayPause();
}, },
iconSize: iconSize,
icon: AnimatedIcon( icon: AnimatedIcon(
icon: AnimatedIcons.play_pause, icon: AnimatedIcons.play_pause,
progress: playPauseController, progress: playPauseController,
size: 50,
), ),
), ),
ProcessingState.idle => const SizedBox.shrink(), ProcessingState.idle => const SizedBox.shrink(),
@ -227,56 +196,130 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
class AudiobookTotalProgressBar extends HookConsumerWidget { class AudiobookTotalProgressBar extends HookConsumerWidget {
const AudiobookTotalProgressBar({ const AudiobookTotalProgressBar({
super.key, 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(audiobookPlayerProvider);
// final playerState = useState(player.processingState); final position = useStream(
// add a listener to the player state player.positionStream,
// player.processingStateStream.listen((state) { initialData: const Duration(seconds: 0),
// playerState.value = state; );
// }); final buffered = useStream(
return StreamBuilder( player.bufferedPositionStream,
stream: player.currentIndexStream, initialData: const Duration(seconds: 0),
builder: (context, currentTrackIndex) { );
return StreamBuilder( final currentIndex = useStream(
stream: player.positionStream, player.currentIndexStream,
builder: (context, progress) { initialData: 0,
// totalProgress is the sum of the duration of all the tracks before the current track + the current track position );
final totalProgress = final durationOfPreviousTracks =
player.book?.tracks.sublist(0, currentTrackIndex.data).fold( player.book?.tracks.sublist(0, currentIndex.data).fold(
const Duration(seconds: 0), const Duration(seconds: 0),
(previousValue, element) => (previousValue, element) => previousValue + element.duration,
previousValue + element.duration,
) ?? ) ??
const Duration(seconds: 0) + const Duration(seconds: 0);
(progress.data ?? const Duration(seconds: 0)); final totalProgress = durationOfPreviousTracks +
(position.data ?? const Duration(seconds: 0));
return StreamBuilder( final totalBuffered = durationOfPreviousTracks +
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) +
(buffered.data ?? const Duration(seconds: 0)); (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( return ProgressBar(
progress: totalProgress, progress: currentChapterProgress ?? totalProgress,
total: player.book?.duration ?? const Duration(seconds: 0), total: currentChapter == null
onSeek: player.seek, ? 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, thumbRadius: 8,
buffered: totalBuffered, buffered: currentChapterBuffered ?? totalBuffered,
bufferedBarColor: Theme.of(context).colorScheme.secondary, 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: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({ const PlayerWhenExpanded({
super.key, super.key,
required this.imgPaddingLeft,
required this.imgPaddingVertical,
required this.imageSize, required this.imageSize,
required this.img, required this.img,
required this.percentageExpandedPlayer, required this.percentageExpandedPlayer,
required this.text, required this.playPauseController,
required this.buttonSkipBackwards,
required this.playPauseButton,
required this.buttonSkipForward,
required this.progressIndicator,
}); });
/// padding values control the position of the image /// padding values control the position of the image
final double imgPaddingLeft;
final double imgPaddingVertical;
final double imageSize; final double imageSize;
final Widget img; final Widget img;
final double percentageExpandedPlayer; final double percentageExpandedPlayer;
final Text text; final AnimationController playPauseController;
final IconButton buttonSkipBackwards;
final Widget playPauseButton;
final IconButton buttonSkipForward;
final Widget progressIndicator;
@override @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( return Column(
children: [ children: [
Align( // sized box for system status bar
alignment: Alignment.centerLeft, 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( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
left: imgPaddingLeft, child: Row(
top: imgPaddingVertical, crossAxisAlignment: CrossAxisAlignment.center,
// bottom: paddingVertical, 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( child: SizedBox(
height: imageSize, height: imageSize,
child: InkWell( child: InkWell(
onTap: () {}, onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular * earlyPercentage,
),
child: img, child: img,
), ),
), ),
), ),
), ),
Expanded( ),
),
// the chapter title
currentChapter == null
? const SizedBox()
: Opacity(
opacity: earlyPercentage,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 33), padding: EdgeInsets.only(
child: Opacity( top: AppElementSizes.paddingRegular * 4 * earlyPercentage,
opacity: percentageExpandedPlayer, // horizontal: 16.0,
child: Column( ),
// 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, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Flexible(child: text), // speed control
Flexible( IconButton(
child: Row( icon: const Icon(Icons.speed),
mainAxisAlignment: MainAxisAlignment.center, onPressed: () {},
children: [ ),
buttonSkipBackwards, // sleep timer
playPauseButton, IconButton(
buttonSkipForward, 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/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: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/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 { class PlayerWhenMinimized extends HookConsumerWidget {
const PlayerWhenMinimized({ const PlayerWhenMinimized({
super.key, super.key,
required this.availWidth,
required this.maxImgSize, required this.maxImgSize,
required this.imgWidget, required this.imgWidget,
required this.elementOpacity, required this.playPauseController,
required this.playPauseButton, required this.percentageMiniplayer,
required this.progressIndicator,
}); });
final double availWidth;
final double maxImgSize; final double maxImgSize;
final Widget imgWidget; final Widget imgWidget;
final double elementOpacity; final AnimationController playPauseController;
final Widget playPauseButton;
final Widget progressIndicator; /// 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(audiobookPlayerProvider);
final controller = ref.watch(miniplayerControllerProvider); final vanishingPercentage = 1 - percentageMiniplayer;
return Column( final progress =
useStream(player.positionStream, initialData: Duration.zero);
final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
var barHeight = vanishingPercentage * 3;
return Stack(
alignment: Alignment.bottomCenter,
children: [ children: [
Expanded( Row(
child: Row(
children: [ children: [
ConstrainedBox( // image
constraints: BoxConstraints(maxHeight: maxImgSize), 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, child: imgWidget,
), ),
),
),
// author and title of the book
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 10), padding: const EdgeInsets.only(left: 8),
child: Opacity(
opacity: elementOpacity,
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(
Text( Text(
player.book?.metadata.title ?? '', bookMetaExpanded?.title ?? '',
style: Theme.of(context) maxLines: 1, overflow: TextOverflow.ellipsis,
.textTheme // velocity:
.bodyMedium! // const Velocity(pixelsPerSecond: Offset(16, 0)),
.copyWith(fontSize: 16), style: Theme.of(context).textTheme.bodyLarge,
), ),
Text( Text(
'audioObject.subtitle', bookMetaExpanded?.authorName ?? '',
style: maxLines: 1,
Theme.of(context).textTheme.bodyMedium!.copyWith( overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context) color: Theme.of(context)
.textTheme .colorScheme
.bodyMedium! .onBackground
.color! .withOpacity(0.7),
.withOpacity(0.55),
), ),
), ),
], ],
), ),
), ),
), ),
),
// IconButton( // IconButton(
// icon: const Icon(Icons.fullscreen), // icon: const Icon(Icons.fullscreen),
// onPressed: () { // onPressed: () {
// controller.animateToHeight(state: PanelState.MAX); // controller.animateToHeight(state: PanelState.MAX);
// }, // },
// ), // ),
Padding( // rewind button
padding: const EdgeInsets.all(8), Opacity(
child: Opacity( opacity: vanishingPercentage,
opacity: elementOpacity, child: Padding(
child: playPauseButton, 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({ const factory PlayerSettings({
@Default(MinimizedPlayerSettings()) @Default(MinimizedPlayerSettings())
MinimizedPlayerSettings miniPlayerSettings, MinimizedPlayerSettings miniPlayerSettings,
@Default(ExpandedPlayerSettings())
ExpandedPlayerSettings expandedPlayerSettings,
@Default(1) double preferredVolume,
@Default(1) double preferredSpeed,
@Default(Duration(minutes: 15)) Duration sleepTimer,
}) = _PlayerSettings; }) = _PlayerSettings;
factory PlayerSettings.fromJson(Map<String, dynamic> json) => factory PlayerSettings.fromJson(Map<String, dynamic> json) =>
_$PlayerSettingsFromJson(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 @freezed
class MinimizedPlayerSettings with _$MinimizedPlayerSettings { class MinimizedPlayerSettings with _$MinimizedPlayerSettings {
const factory MinimizedPlayerSettings({ const factory MinimizedPlayerSettings({
@Default(false) bool useChapterInfo, @Default(false) bool useChapterInfo,
}) = _MiniPlayerSettings; }) = _MinimizedPlayerSettings;
factory MinimizedPlayerSettings.fromJson(Map<String, dynamic> json) => factory MinimizedPlayerSettings.fromJson(Map<String, dynamic> json) =>
_$MinimizedPlayerSettingsFromJson(json); _$MinimizedPlayerSettingsFromJson(json);

View file

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

View file

@ -30,22 +30,49 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map<String, dynamic> json) =>
? const MinimizedPlayerSettings() ? const MinimizedPlayerSettings()
: MinimizedPlayerSettings.fromJson( : MinimizedPlayerSettings.fromJson(
json['miniPlayerSettings'] as Map<String, dynamic>), 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( Map<String, dynamic> _$$PlayerSettingsImplToJson(
_$PlayerSettingsImpl instance) => _$PlayerSettingsImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'miniPlayerSettings': instance.miniPlayerSettings, 'miniPlayerSettings': instance.miniPlayerSettings,
'expandedPlayerSettings': instance.expandedPlayerSettings,
'preferredVolume': instance.preferredVolume,
'preferredSpeed': instance.preferredSpeed,
'sleepTimer': instance.sleepTimer.inMicroseconds,
}; };
_$MiniPlayerSettingsImpl _$$MiniPlayerSettingsImplFromJson( _$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson(
Map<String, dynamic> json) => 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, useChapterInfo: json['useChapterInfo'] as bool? ?? false,
); );
Map<String, dynamic> _$$MiniPlayerSettingsImplToJson( Map<String, dynamic> _$$MinimizedPlayerSettingsImplToJson(
_$MiniPlayerSettingsImpl instance) => _$MinimizedPlayerSettingsImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'useChapterInfo': instance.useChapterInfo, '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" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" 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: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -390,6 +398,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.2" 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: flutter_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
@ -406,6 +422,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" 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: flutter_riverpod:
dependency: transitive dependency: transitive
description: description:
@ -552,6 +584,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.7" 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: io:
dependency: transitive dependency: transitive
description: description:
@ -751,6 +799,14 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.3" 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: octo_image:
dependency: transitive dependency: transitive
description: description:

View file

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