diff --git a/.vscode/settings.json b/.vscode/settings.json index 24c9cdb..fc25963 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,8 @@ "cSpell.words": [ "audioplayers", "Autovalidate", + "fullscreen", + "miniplayer", "mocktail", "riverpod", "shelfsdk", diff --git a/lib/extensions/hero_tag_conventions.dart b/lib/constants/hero_tag_conventions.dart similarity index 100% rename from lib/extensions/hero_tag_conventions.dart rename to lib/constants/hero_tag_conventions.dart diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index 274a8f7..c245f03 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -8,15 +8,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:whispering_pages/api/image_provider.dart'; import 'package:whispering_pages/api/library_item_provider.dart'; -import 'package:whispering_pages/extensions/hero_tag_conventions.dart'; +import 'package:whispering_pages/constants/hero_tag_conventions.dart'; import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart'; import 'package:whispering_pages/router/models/library_item_extras.dart'; import 'package:whispering_pages/settings/app_settings_provider.dart'; import 'package:whispering_pages/theme/theme_from_cover_provider.dart'; -import 'package:whispering_pages/widgets/shelves/book_shelf.dart'; +import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart'; -import '../../../widgets/expandable_description.dart'; -import '../../../widgets/library_item_sliver_app_bar.dart'; +import '../../../shared/widgets/expandable_description.dart'; +import 'library_item_sliver_app_bar.dart'; class LibraryItemPage extends HookConsumerWidget { const LibraryItemPage({ diff --git a/lib/widgets/library_item_sliver_app_bar.dart b/lib/features/item_viewer/view/library_item_sliver_app_bar.dart similarity index 100% rename from lib/widgets/library_item_sliver_app_bar.dart rename to lib/features/item_viewer/view/library_item_sliver_app_bar.dart diff --git a/lib/pages/onboarding/onboarding_single_page.dart b/lib/features/onboarding/view/onboarding_single_page.dart similarity index 98% rename from lib/pages/onboarding/onboarding_single_page.dart rename to lib/features/onboarding/view/onboarding_single_page.dart index de7887c..ae170d1 100644 --- a/lib/pages/onboarding/onboarding_single_page.dart +++ b/lib/features/onboarding/view/onboarding_single_page.dart @@ -10,8 +10,8 @@ import 'package:whispering_pages/api/server_provider.dart'; import 'package:whispering_pages/router/router.dart'; import 'package:whispering_pages/settings/api_settings_provider.dart'; import 'package:whispering_pages/settings/models/models.dart' as model; -import 'package:whispering_pages/widgets/add_new_server.dart'; -import 'package:whispering_pages/widgets/user_login.dart'; +import 'package:whispering_pages/shared/widgets/add_new_server.dart'; +import 'package:whispering_pages/features/onboarding/view/user_login.dart'; class OnboardingSinglePage extends HookConsumerWidget { const OnboardingSinglePage({ diff --git a/lib/widgets/user_login.dart b/lib/features/onboarding/view/user_login.dart similarity index 100% rename from lib/widgets/user_login.dart rename to lib/features/onboarding/view/user_login.dart diff --git a/lib/features/player/audiobook_payer.dart b/lib/features/player/core/audiobook_payer.dart similarity index 78% rename from lib/features/player/audiobook_payer.dart rename to lib/features/player/core/audiobook_payer.dart index 22d5701..bd431a4 100644 --- a/lib/features/player/audiobook_payer.dart +++ b/lib/features/player/core/audiobook_payer.dart @@ -9,7 +9,7 @@ import 'package:shelfsdk/audiobookshelf_api.dart'; /// will manage the audio player instance class AudiobookPlayer extends AudioPlayer { // constructor which takes in the BookExpanded object - AudiobookPlayer(this.token, this.baseUrl) : super() { + AudiobookPlayer(this.token, this.baseUrl, {super.playerId}) : super() { // set the source of the player to the first track in the book } @@ -31,12 +31,20 @@ class AudiobookPlayer extends AudioPlayer { final int _currentIndex = 0; /// sets the current [AudioTrack] as the source of the player - Future setSourceAudioBook(BookExpanded book) async { + Future setSourceAudioBook(BookExpanded? book) async { + // if the book is null, stop the player + if (book == null) { + _book = null; + return stop(); + } + // see if the book is the same as the current book if (_book == book) { // if the book is the same, do nothing return; } + // first stop the player + await stop(); var track = book.tracks[_currentIndex]; var url = '$baseUrl${track.contentUrl}?token=$token'; @@ -64,9 +72,14 @@ class AudiobookPlayer extends AudioPlayer { PlayerState.disposed => throw StateError('Player is disposed'), }; } + + /// override resume to set the source if the book is not set + @override + Future resume() async { + if (_book == null) { + throw StateError('No book is set'); + } + return super.resume(); + } } -void main(List args) { - final AudiobookPlayer abPlayer = AudiobookPlayer('', Uri.parse('')); - print(abPlayer.resume()); -} diff --git a/lib/features/player/providers/audiobook_player_provider.dart b/lib/features/player/providers/audiobook_player_provider.dart index 7759744..f0e61d7 100644 --- a/lib/features/player/providers/audiobook_player_provider.dart +++ b/lib/features/player/providers/audiobook_player_provider.dart @@ -1,6 +1,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:whispering_pages/api/api_provider.dart'; -import 'package:whispering_pages/features/player/audiobook_payer.dart' as abp; +import 'package:whispering_pages/features/player/core/audiobook_payer.dart' as abp; part 'audiobook_player_provider.g.dart'; @@ -16,15 +16,23 @@ part 'audiobook_player_provider.g.dart'; // return player; // } +const playerId = 'audiobook_player'; + @Riverpod(keepAlive: true) class AudiobookPlayer extends _$AudiobookPlayer { @override abp.AudiobookPlayer build() { final api = ref.watch(authenticatedApiProvider); - final player = abp.AudiobookPlayer(api.token!, api.baseUrl); + final player = + abp.AudiobookPlayer(api.token!, api.baseUrl, playerId: playerId); ref.onDispose(player.dispose); + // bind notify listeners to the player + player.onPlayerStateChanged.listen((_) { + notifyListeners(); + }); + return player; } diff --git a/lib/features/player/providers/audiobook_player_provider.g.dart b/lib/features/player/providers/audiobook_player_provider.g.dart index e4eb1bf..27ed916 100644 --- a/lib/features/player/providers/audiobook_player_provider.g.dart +++ b/lib/features/player/providers/audiobook_player_provider.g.dart @@ -6,7 +6,7 @@ part of 'audiobook_player_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$audiobookPlayerHash() => r'8cbadcb264382300e63b3dbaf167a3bea1638a6e'; +String _$audiobookPlayerHash() => r'a636d5e8e73dc6bbf7b3f47f83884bb3af3b9370'; /// See also [AudiobookPlayer]. @ProviderFor(AudiobookPlayer) diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart new file mode 100644 index 0000000..9a77793 --- /dev/null +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -0,0 +1,11 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart'; + +part 'currently_playing_provider.g.dart'; + +@riverpod +BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) { + final player = ref.watch(audiobookPlayerProvider); + return player.book; +} diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart new file mode 100644 index 0000000..2b17d37 --- /dev/null +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'currently_playing_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$currentlyPlayingBookHash() => + r'c777ea8b463d8441a0da5e08b4c41b501ce68aad'; + +/// See also [currentlyPlayingBook]. +@ProviderFor(currentlyPlayingBook) +final currentlyPlayingBookProvider = + AutoDisposeProvider.internal( + currentlyPlayingBook, + name: r'currentlyPlayingBookProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentlyPlayingBookHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef CurrentlyPlayingBookRef = 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/view/audioobok_player.dart b/lib/features/player/view/audioobok_player.dart new file mode 100644 index 0000000..abed71b --- /dev/null +++ b/lib/features/player/view/audioobok_player.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:miniplayer/miniplayer.dart'; +import 'package:whispering_pages/api/image_provider.dart'; +import 'package:whispering_pages/api/library_item_provider.dart'; +import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart'; +import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart'; +import 'package:whispering_pages/shared/widgets/shelves/book_shelf.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); +} + +const double playerMinHeight = 70; +const double playerMaxHeight = 370; +const miniplayerPercentageDeclaration = 0.2; +final ValueNotifier playerExpandProgress = + ValueNotifier(playerMinHeight); + +final MiniplayerController controller = MiniplayerController(); + +class AudiobookPlayer extends HookConsumerWidget { + const AudiobookPlayer({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentBook = ref.watch(currentlyPlayingBookProvider); + if (currentBook == null) { + return const SizedBox.shrink(); + } + final item = ref.watch(libraryItemProvider(currentBook.libraryItemId)); + final player = ref.watch(audiobookPlayerProvider); + final image = item.valueOrNull != null + ? ref.watch( + coverImageProvider(item.valueOrNull!), + ) + : null; + final imgWidget = image?.valueOrNull != null + ? Image.memory( + image!.valueOrNull!, + fit: BoxFit.cover, + ) + : const BookCoverSkeleton(); + return Miniplayer( + valueNotifier: playerExpandProgress, + minHeight: playerMinHeight, + maxHeight: playerMaxHeight, + controller: controller, + elevation: 4, + onDismissed: () { + player.setSourceAudioBook(null); + }, + curve: Curves.easeOut, + builder: (height, percentage) { + final bool isFormMiniplayer = + percentage < miniplayerPercentageDeclaration; + final double availWidth = MediaQuery.of(context).size.width; + final maxImgSize = availWidth * 0.4; + + final text = Text(player.book?.metadata.title ?? ''); + const buttonPlay = IconButton( + icon: Icon(Icons.pause), + onPressed: onTap, + ); + const progressIndicator = LinearProgressIndicator(value: 0.3); + + //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: 10, + 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, + ); + const buttonPlayExpanded = IconButton( + icon: Icon(Icons.pause_circle_filled), + iconSize: 50, + onPressed: onTap, + ); + + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only( + left: paddingLeft, + top: paddingVertical, + bottom: paddingVertical, + ), + child: SizedBox( + height: imageSize, + child: imgWidget, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 33), + child: Opacity( + opacity: percentageExpandedPlayer, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Flexible(child: text), + const Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buttonSkipBackwards, + buttonPlayExpanded, + buttonSkipForward, + ], + ), + ), + const Flexible(child: progressIndicator), + Container(), + Container(), + ], + ), + ), + ), + ), + ], + ); + } + + //Miniplayer + final percentageMiniplayer = percentageFromValueInRange( + min: playerMinHeight, + max: playerMaxHeight * miniplayerPercentageDeclaration + + playerMinHeight, + value: height, + ); + + final elementOpacity = 1 - 1 * percentageMiniplayer; + final progressIndicatorHeight = 4 - 4 * percentageMiniplayer; + + return Column( + children: [ + Expanded( + child: Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxImgSize), + child: imgWidget, + ), + 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), + ), + ), + ], + ), + ), + ), + ), + IconButton( + icon: const Icon(Icons.fullscreen), + onPressed: () { + controller.animateToHeight(state: PanelState.MAX); + }, + ), + Padding( + padding: const EdgeInsets.only(right: 3), + child: Opacity( + opacity: elementOpacity, + child: buttonPlay, + ), + ), + ], + ), + ), + SizedBox( + height: progressIndicatorHeight, + child: Opacity( + opacity: elementOpacity, + child: progressIndicator, + ), + ), + ], + ); + }, + ); + } +} + +void onTap() {} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index c0fc365..2707163 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:whispering_pages/api/api_provider.dart'; import 'package:whispering_pages/settings/app_settings_provider.dart'; -import '../widgets/drawer.dart'; -import '../widgets/shelves/home_shelf.dart'; +import '../shared/widgets/drawer.dart'; +import '../shared/widgets/shelves/home_shelf.dart'; class HomePage extends HookConsumerWidget { const HomePage({super.key}); diff --git a/lib/pages/library_page.dart b/lib/pages/library_page.dart index 293e6fa..ad524ff 100644 --- a/lib/pages/library_page.dart +++ b/lib/pages/library_page.dart @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:whispering_pages/api/api_provider.dart'; import 'package:whispering_pages/settings/api_settings_provider.dart'; -import '../widgets/drawer.dart'; -import '../widgets/shelves/home_shelf.dart'; +import '../shared/widgets/drawer.dart'; +import '../shared/widgets/shelves/home_shelf.dart'; // TODO: implement the library page class LibraryPage extends HookConsumerWidget { diff --git a/lib/pages/server_manager.dart b/lib/pages/server_manager.dart index f33a457..92ec9c5 100644 --- a/lib/pages/server_manager.dart +++ b/lib/pages/server_manager.dart @@ -5,7 +5,7 @@ import 'package:whispering_pages/api/authenticated_user_provider.dart'; import 'package:whispering_pages/api/server_provider.dart'; import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model; import 'package:whispering_pages/settings/api_settings_provider.dart'; -import 'package:whispering_pages/widgets/add_new_server.dart'; +import 'package:whispering_pages/shared/widgets/add_new_server.dart'; class ServerManagerPage extends HookConsumerWidget { const ServerManagerPage({ diff --git a/lib/router/router.dart b/lib/router/router.dart index 4545727..a4d1224 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -4,7 +4,7 @@ import 'package:whispering_pages/pages/app_settings.dart'; import 'package:whispering_pages/pages/home_page.dart'; import 'package:whispering_pages/features/item_viewer/view/library_item_page.dart'; import 'package:whispering_pages/pages/library_page.dart'; -import 'package:whispering_pages/pages/onboarding/onboarding_single_page.dart'; +import 'package:whispering_pages/features/onboarding/view/onboarding_single_page.dart'; import 'scaffold_with_nav_bar.dart'; import 'transitions/slide.dart'; @@ -28,6 +28,7 @@ class MyAppRouter { name: Routes.onboarding.name, builder: (context, state) => const OnboardingSinglePage(), ), + // The main app shell StatefulShellRoute.indexedStack( builder: ( BuildContext context, diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index f39a9ec..7e8df96 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:whispering_pages/features/player/view/audioobok_player.dart'; /// Builds the "shell" for the app by building a Scaffold with a /// BottomNavigationBar, where [child] is placed in the body of the Scaffold. -class ScaffoldWithNavBar extends StatelessWidget { +class ScaffoldWithNavBar extends HookConsumerWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ required this.navigationShell, @@ -14,9 +16,14 @@ class ScaffoldWithNavBar extends StatelessWidget { final StatefulNavigationShell navigationShell; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( - body: navigationShell, + body: Stack( + children: [ + navigationShell, + const AudiobookPlayer(), + ], + ), bottomNavigationBar: BottomNavigationBar( elevation: 0.0, landscapeLayout: BottomNavigationBarLandscapeLayout.centered, diff --git a/lib/widgets/add_new_server.dart b/lib/shared/widgets/add_new_server.dart similarity index 100% rename from lib/widgets/add_new_server.dart rename to lib/shared/widgets/add_new_server.dart diff --git a/lib/widgets/drawer.dart b/lib/shared/widgets/drawer.dart similarity index 100% rename from lib/widgets/drawer.dart rename to lib/shared/widgets/drawer.dart diff --git a/lib/widgets/expandable_description.dart b/lib/shared/widgets/expandable_description.dart similarity index 100% rename from lib/widgets/expandable_description.dart rename to lib/shared/widgets/expandable_description.dart diff --git a/lib/widgets/shelves/author_shelf.dart b/lib/shared/widgets/shelves/author_shelf.dart similarity index 96% rename from lib/widgets/shelves/author_shelf.dart rename to lib/shared/widgets/shelves/author_shelf.dart index 3a5e066..be0f0e1 100644 --- a/lib/widgets/shelves/author_shelf.dart +++ b/lib/shared/widgets/shelves/author_shelf.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:whispering_pages/api/image_provider.dart'; -import 'package:whispering_pages/widgets/shelves/home_shelf.dart'; +import 'package:whispering_pages/shared/widgets/shelves/home_shelf.dart'; /// A shelf that displays Authors on the home page class AuthorHomeShelf extends HookConsumerWidget { diff --git a/lib/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart similarity index 97% rename from lib/widgets/shelves/book_shelf.dart rename to lib/shared/widgets/shelves/book_shelf.dart index 69a22a1..9ac5643 100644 --- a/lib/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shimmer/shimmer.dart' show Shimmer; import 'package:whispering_pages/api/image_provider.dart'; -import 'package:whispering_pages/extensions/hero_tag_conventions.dart'; +import 'package:whispering_pages/constants/hero_tag_conventions.dart'; import 'package:whispering_pages/router/models/library_item_extras.dart'; import 'package:whispering_pages/router/router.dart'; -import 'package:whispering_pages/widgets/shelves/home_shelf.dart'; +import 'package:whispering_pages/shared/widgets/shelves/home_shelf.dart'; /// A shelf that displays books on the home page class BookHomeShelf extends HookConsumerWidget { diff --git a/lib/widgets/shelves/home_shelf.dart b/lib/shared/widgets/shelves/home_shelf.dart similarity index 95% rename from lib/widgets/shelves/home_shelf.dart rename to lib/shared/widgets/shelves/home_shelf.dart index 1e7fa02..a34a2db 100644 --- a/lib/widgets/shelves/home_shelf.dart +++ b/lib/shared/widgets/shelves/home_shelf.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:whispering_pages/widgets/shelves/author_shelf.dart'; -import 'package:whispering_pages/widgets/shelves/book_shelf.dart'; +import 'package:whispering_pages/shared/widgets/shelves/author_shelf.dart'; +import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart'; /// A shelf that displays books/authors/series on the home page /// diff --git a/pubspec.lock b/pubspec.lock index c353acd..00dbdcb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -688,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + miniplayer: + dependency: "direct main" + description: + name: miniplayer + sha256: "6e12c27aef7432fc16508460a6dc824f3edfeb01761bd0dbfbccc84d516121bf" + url: "https://pub.dev" + source: hosted + version: "1.0.1" octo_image: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c3dc892..957a1a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: isar_flutter_libs: ^4.0.0-dev.13 json_annotation: ^4.9.0 lottie: ^3.1.0 + miniplayer: ^1.0.1 path: ^1.9.0 path_provider: ^2.1.0 riverpod_annotation: ^2.3.5