diff --git a/.vscode/settings.json b/.vscode/settings.json index 73fb4a2..d4418df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "audioplayers", "Autovalidate", "fullscreen", + "Lerp", "miniplayer", "mocktail", "riverpod", diff --git a/lib/constants/sizes.dart b/lib/constants/sizes.dart new file mode 100644 index 0000000..ad45272 --- /dev/null +++ b/lib/constants/sizes.dart @@ -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; +} diff --git a/lib/db/player_prefs/book_prefs.dart b/lib/db/player_prefs/book_prefs.dart new file mode 100644 index 0000000..b4e3807 --- /dev/null +++ b/lib/db/player_prefs/book_prefs.dart @@ -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, + }); +} diff --git a/lib/db/player_prefs/book_prefs.g.dart b/lib/db/player_prefs/book_prefs.g.dart new file mode 100644 index 0000000..ee012a5 --- /dev/null +++ b/lib/db/player_prefs/book_prefs.g.dart @@ -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 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( + 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 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 libItemId, + double? speed, + }); +} + +class _BookPrefsUpdateAllImpl implements _BookPrefsUpdateAll { + const _BookPrefsUpdateAllImpl(this.collection); + + final IsarCollection collection; + + @override + int call({ + required List libItemId, + Object? speed = ignore, + }) { + return collection.updateProperties(libItemId, { + if (speed != ignore) 1: speed as double?, + }); + } +} + +extension BookPrefsUpdate on IsarCollection { + _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 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 { + _BookPrefsQueryUpdate get updateFirst => + _BookPrefsQueryUpdateImpl(this, limit: 1); + + _BookPrefsQueryUpdate get updateAll => _BookPrefsQueryUpdateImpl(this); +} + +class _BookPrefsQueryBuilderUpdateImpl implements _BookPrefsQueryUpdate { + const _BookPrefsQueryBuilderUpdateImpl(this.query, {this.limit}); + + final QueryBuilder 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 { + _BookPrefsQueryUpdate get updateFirst => + _BookPrefsQueryBuilderUpdateImpl(this, limit: 1); + + _BookPrefsQueryUpdate get updateAll => _BookPrefsQueryBuilderUpdateImpl(this); +} + +extension BookPrefsQueryFilter + on QueryBuilder { + QueryBuilder libItemIdEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 0, + value: value, + ), + ); + }); + } + + QueryBuilder + libItemIdGreaterThan( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 0, + value: value, + ), + ); + }); + } + + QueryBuilder + libItemIdGreaterThanOrEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 0, + value: value, + ), + ); + }); + } + + QueryBuilder libItemIdLessThan( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 0, + value: value, + ), + ); + }); + } + + QueryBuilder + libItemIdLessThanOrEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 0, + value: value, + ), + ); + }); + } + + QueryBuilder libItemIdBetween( + int lower, + int upper, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 0, + lower: lower, + upper: upper, + ), + ); + }); + } + + QueryBuilder speedIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const IsNullCondition(property: 1)); + }); + } + + QueryBuilder speedIsNotNull() { + return QueryBuilder.apply(not(), (query) { + return query.addFilterCondition(const IsNullCondition(property: 1)); + }); + } + + QueryBuilder speedEqualTo( + double? value, { + double epsilon = Filter.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 1, + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder speedGreaterThan( + double? value, { + double epsilon = Filter.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 1, + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + speedGreaterThanOrEqualTo( + double? value, { + double epsilon = Filter.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 1, + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder speedLessThan( + double? value, { + double epsilon = Filter.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 1, + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + speedLessThanOrEqualTo( + double? value, { + double epsilon = Filter.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 1, + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder 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 {} + +extension BookPrefsQuerySortBy on QueryBuilder { + QueryBuilder sortByLibItemId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(0); + }); + } + + QueryBuilder sortByLibItemIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(0, sort: Sort.desc); + }); + } + + QueryBuilder sortBySpeed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(1); + }); + } + + QueryBuilder sortBySpeedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(1, sort: Sort.desc); + }); + } +} + +extension BookPrefsQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByLibItemId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(0); + }); + } + + QueryBuilder thenByLibItemIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(0, sort: Sort.desc); + }); + } + + QueryBuilder thenBySpeed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(1); + }); + } + + QueryBuilder thenBySpeedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(1, sort: Sort.desc); + }); + } +} + +extension BookPrefsQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctBySpeed() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(1); + }); + } +} + +extension BookPrefsQueryProperty1 + on QueryBuilder { + QueryBuilder libItemIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(0); + }); + } + + QueryBuilder speedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(1); + }); + } +} + +extension BookPrefsQueryProperty2 + on QueryBuilder { + QueryBuilder libItemIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(0); + }); + } + + QueryBuilder speedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(1); + }); + } +} + +extension BookPrefsQueryProperty3 + on QueryBuilder { + QueryBuilder libItemIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(0); + }); + } + + QueryBuilder speedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(1); + }); + } +} diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 8e8a78d..f62557e 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -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.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 setSourceAudioBook(BookExpanded? book) async { + Future 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 getDuration() async { - // if (_book == null) { - // return null; - // } - // return _book!.tracks.fold( - // Duration.zero, - // (previousValue, element) => previousValue + element.duration, - // ); -// } - // @override - // Future 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 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 get positionStream { + return super.positionStream.map((position) { + if (_book == null) { + return Duration.zero; + } + return position + sumOfTracks(_book!, sequenceState!.currentIndex); + }); + } + + @override + Stream 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, + ); + } + + + } diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index aad29cd..df6a471 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -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; +// } diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart index 2b17d37..2ac37aa 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -23,5 +23,43 @@ final currentlyPlayingBookProvider = ); typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef; +String _$currentPlayingChapterHash() => + r'562416b7e0068aaba9138cb8e0ed7a5ddba8e6c6'; + +/// provided the current chapter of the book being played +/// +/// Copied from [currentPlayingChapter]. +@ProviderFor(currentPlayingChapter) +final currentPlayingChapterProvider = + AutoDisposeProvider.internal( + currentPlayingChapter, + name: r'currentPlayingChapterProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentPlayingChapterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef CurrentPlayingChapterRef = AutoDisposeProviderRef; +String _$currentBookMetadataHash() => + r'02b462a051fce5bcbdad6fdb708b60256fbb588c'; + +/// provides the book metadata of the currently playing book +/// +/// Copied from [currentBookMetadata]. +@ProviderFor(currentBookMetadata) +final currentBookMetadataProvider = + AutoDisposeProvider.internal( + currentBookMetadata, + name: r'currentBookMetadataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentBookMetadataHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef CurrentBookMetadataRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/player/providers/player_form.dart b/lib/features/player/providers/player_form.dart index 47371f9..0ac34ed 100644 --- a/lib/features/player/providers/player_form.dart +++ b/lib/features/player/providers/player_form.dart @@ -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. diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index f35b418..1d7a4a1 100644 --- a/lib/features/player/view/audiobook_player.dart +++ b/lib/features/player/view/audiobook_player.dart @@ -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; - - const buttonSkipForward = IconButton( - icon: Icon(Icons.forward_30), - iconSize: 33, - onPressed: onTap, - ); - const buttonSkipBackwards = IconButton( - icon: Icon(Icons.replay_10), - iconSize: 33, - onPressed: onTap, - ); + // this calculation needs a refactor + var percentageExpandedPlayer = percentage + .inverseLerp( + miniplayerPercentageDeclaration, + 1, + ) + .clamp(0.0, 1.0); + 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( - const Duration(seconds: 0), - (previousValue, element) => - previousValue + element.duration, - ) ?? - const Duration(seconds: 0) + - (progress.data ?? const Duration(seconds: 0)); + 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, + ) ?? + 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); - 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) + - (buffered.data ?? const Duration(seconds: 0)); - return ProgressBar( - progress: totalProgress, - total: player.book?.duration ?? const Duration(seconds: 0), - onSeek: player.seek, - thumbRadius: 8, - buffered: totalBuffered, - bufferedBarColor: Theme.of(context).colorScheme.secondary, - ); - }, - ); - }, + final currentChapterBuffered = + currentChapter == null ? null : (totalBuffered - currentChapter.start); + + return ProgressBar( + 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: currentChapterBuffered ?? totalBuffered, + bufferedBarColor: Theme.of(context).colorScheme.secondary, + timeLabelType: TimeLabelType.remainingTime, + timeLabelLocation: TimeLabelLocation.below, ); } } diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index 02fe37f..ccbd551 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -1,75 +1,257 @@ 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, - child: Padding( - padding: EdgeInsets.only( - left: imgPaddingLeft, - top: imgPaddingVertical, - // bottom: paddingVertical, - ), - child: SizedBox( - height: imageSize, - child: InkWell( - onTap: () {}, - child: img, + // 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(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: () {}, + ), + ], ), ), ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 33), - child: Opacity( - opacity: percentageExpandedPlayer, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Flexible(child: text), - Flexible( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buttonSkipBackwards, - playPauseButton, - buttonSkipForward, - ], - ), + // 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), ), - Flexible(child: progressIndicator), ], ), + child: SizedBox( + height: imageSize, + child: InkWell( + onTap: () {}, + child: ClipRRect( + borderRadius: BorderRadius.circular( + AppElementSizes.borderRadiusRegular * earlyPercentage, + ), + child: img, + ), + ), + ), + ), + ), + ), + + // the chapter title + currentChapter == null + ? const SizedBox() + : Opacity( + opacity: earlyPercentage, + child: Padding( + 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: [ + // 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: () {}, + ), + ], ), ), ), @@ -77,3 +259,73 @@ class PlayerWhenExpanded extends StatelessWidget { ); } } + +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); + } + } + }, + ); + } +} diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart index 4a9cbe1..a18462c 100644 --- a/lib/features/player/view/player_when_minimized.dart +++ b/lib/features/player/view/player_when_minimized.dart @@ -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( - children: [ - ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxImgSize), - child: imgWidget, + Row( + children: [ + // image + Padding( + padding: EdgeInsets.symmetric( + horizontal: + ((availWidth - maxImgSize) / 2) * percentageMiniplayer, ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Opacity( - opacity: elementOpacity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - player.book?.metadata.title ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 16), - ), - Text( - 'audioObject.subtitle', - style: - Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context) - .textTheme - .bodyMedium! - .color! - .withOpacity(0.55), - ), - ), - ], - ), + 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, ), ), - // IconButton( - // icon: const Icon(Icons.fullscreen), - // onPressed: () { - // controller.animateToHeight(state: PanelState.MAX); - // }, - // ), - Padding( - padding: const EdgeInsets.all(8), - child: Opacity( - opacity: elementOpacity, - child: playPauseButton, + ), + // author and title of the book + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // AutoScrollText( + Text( + bookMetaExpanded?.title ?? '', + maxLines: 1, overflow: TextOverflow.ellipsis, + // velocity: + // const Velocity(pixelsPerSecond: Offset(16, 0)), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + bookMetaExpanded?.authorName ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.7), + ), + ), + ], ), ), - ], + ), + // IconButton( + // icon: const Icon(Icons.fullscreen), + // onPressed: () { + // controller.animateToHeight(state: PanelState.MAX); + // }, + // ), + // rewind button + Opacity( + opacity: vanishingPercentage, + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: IconButton( + icon: const Icon( + Icons.replay_30, + size: AppElementSizes.iconSizeSmall, + ), + onPressed: () {}, + ), + ), + ), + // 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, - // ), - // ), ], ); } diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 7366bdd..bd39c67 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -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 json) => _$PlayerSettingsFromJson(json); } +@freezed +class ExpandedPlayerSettings with _$ExpandedPlayerSettings { + const factory ExpandedPlayerSettings({ + @Default(false) bool showTotalProgress, + @Default(true) bool showChapterProgress, + }) = _ExpandedPlayerSettings; + + factory ExpandedPlayerSettings.fromJson(Map json) => + _$ExpandedPlayerSettingsFromJson(json); +} @freezed class MinimizedPlayerSettings with _$MinimizedPlayerSettings { const factory MinimizedPlayerSettings({ @Default(false) bool useChapterInfo, - }) = _MiniPlayerSettings; + }) = _MinimizedPlayerSettings; factory MinimizedPlayerSettings.fromJson(Map json) => _$MinimizedPlayerSettingsFromJson(json); diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index c2597e7..7cd566d 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -224,6 +224,11 @@ PlayerSettings _$PlayerSettingsFromJson(Map 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 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 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 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 json) { + return _ExpandedPlayerSettings.fromJson(json); +} + +/// @nodoc +mixin _$ExpandedPlayerSettings { + bool get showTotalProgress => throw _privateConstructorUsedError; + bool get showChapterProgress => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ExpandedPlayerSettingsCopyWith 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 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 toJson() { + return _$$ExpandedPlayerSettingsImplToJson( + this, + ); + } +} + +abstract class _ExpandedPlayerSettings implements ExpandedPlayerSettings { + const factory _ExpandedPlayerSettings( + {final bool showTotalProgress, + final bool showChapterProgress}) = _$ExpandedPlayerSettingsImpl; + + factory _ExpandedPlayerSettings.fromJson(Map json) = + _$ExpandedPlayerSettingsImpl.fromJson; + + @override + bool get showTotalProgress; + @override + bool get showChapterProgress; + @override + @JsonKey(ignore: true) + _$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl> + get copyWith => throw _privateConstructorUsedError; +} + MinimizedPlayerSettings _$MinimizedPlayerSettingsFromJson( Map 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 json) => - _$$MiniPlayerSettingsImplFromJson(json); + factory _$MinimizedPlayerSettingsImpl.fromJson(Map 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 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 json) = - _$MiniPlayerSettingsImpl.fromJson; + factory _MinimizedPlayerSettings.fromJson(Map json) = + _$MinimizedPlayerSettingsImpl.fromJson; @override bool get useChapterInfo; @override @JsonKey(ignore: true) - _$$MiniPlayerSettingsImplCopyWith<_$MiniPlayerSettingsImpl> get copyWith => - throw _privateConstructorUsedError; + _$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl> + get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index b9fdaef..278da73 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -30,22 +30,49 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => ? const MinimizedPlayerSettings() : MinimizedPlayerSettings.fromJson( json['miniPlayerSettings'] as Map), + expandedPlayerSettings: json['expandedPlayerSettings'] == null + ? const ExpandedPlayerSettings() + : ExpandedPlayerSettings.fromJson( + json['expandedPlayerSettings'] as Map), + 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 _$$PlayerSettingsImplToJson( _$PlayerSettingsImpl instance) => { 'miniPlayerSettings': instance.miniPlayerSettings, + 'expandedPlayerSettings': instance.expandedPlayerSettings, + 'preferredVolume': instance.preferredVolume, + 'preferredSpeed': instance.preferredSpeed, + 'sleepTimer': instance.sleepTimer.inMicroseconds, }; -_$MiniPlayerSettingsImpl _$$MiniPlayerSettingsImplFromJson( +_$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson( Map json) => - _$MiniPlayerSettingsImpl( + _$ExpandedPlayerSettingsImpl( + showTotalProgress: json['showTotalProgress'] as bool? ?? false, + showChapterProgress: json['showChapterProgress'] as bool? ?? true, + ); + +Map _$$ExpandedPlayerSettingsImplToJson( + _$ExpandedPlayerSettingsImpl instance) => + { + 'showTotalProgress': instance.showTotalProgress, + 'showChapterProgress': instance.showChapterProgress, + }; + +_$MinimizedPlayerSettingsImpl _$$MinimizedPlayerSettingsImplFromJson( + Map json) => + _$MinimizedPlayerSettingsImpl( useChapterInfo: json['useChapterInfo'] as bool? ?? false, ); -Map _$$MiniPlayerSettingsImplToJson( - _$MiniPlayerSettingsImpl instance) => +Map _$$MinimizedPlayerSettingsImplToJson( + _$MinimizedPlayerSettingsImpl instance) => { 'useChapterInfo': instance.useChapterInfo, }; diff --git a/lib/shared/extensions/inverse_lerp.dart b/lib/shared/extensions/inverse_lerp.dart new file mode 100644 index 0000000..09a994d --- /dev/null +++ b/lib/shared/extensions/inverse_lerp.dart @@ -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); + } +} diff --git a/pubspec.lock b/pubspec.lock index 428a520..c6f74a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 68b78b2..6b0e6d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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