diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index c245f03..72d8053 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -9,7 +9,7 @@ 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/constants/hero_tag_conventions.dart'; -import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart'; +import 'package:whispering_pages/features/player/providers/audiobook_player.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'; diff --git a/lib/features/player/core/audiobook_payer.dart b/lib/features/player/core/audiobook_player.dart similarity index 64% rename from lib/features/player/core/audiobook_payer.dart rename to lib/features/player/core/audiobook_player.dart index bd431a4..6adbb3e 100644 --- a/lib/features/player/core/audiobook_payer.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -30,6 +30,9 @@ class AudiobookPlayer extends AudioPlayer { // the current index of the audio file in the [book] final int _currentIndex = 0; + // available audio tracks + int? get availableTracks => _book?.tracks.length; + /// sets the current [AudioTrack] as the source of the player Future setSourceAudioBook(BookExpanded? book) async { // if the book is null, stop the player @@ -50,7 +53,6 @@ class AudiobookPlayer extends AudioPlayer { var url = '$baseUrl${track.contentUrl}?token=$token'; await setSourceUrl( url, - // '${track.contentUrl}?token=$token', mimeType: track.mimeType, ); _book = book; @@ -81,5 +83,37 @@ class AudiobookPlayer extends AudioPlayer { } return super.resume(); } -} + /// a convenience stream for onPositionEveryXSeconds + Stream onPositionEvery(Duration duration) => TimerPositionUpdater( + getPosition: getCurrentPosition, + interval: duration, + ).positionStream; + + /// 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; + } +} diff --git a/lib/features/player/providers/audiobook_player_provider.dart b/lib/features/player/providers/audiobook_player.dart similarity index 98% rename from lib/features/player/providers/audiobook_player_provider.dart rename to lib/features/player/providers/audiobook_player.dart index f0e61d7..bac5cda 100644 --- a/lib/features/player/providers/audiobook_player_provider.dart +++ b/lib/features/player/providers/audiobook_player.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/core/audiobook_payer.dart' as abp; +import 'package:whispering_pages/features/player/core/audiobook_player.dart' as abp; part 'audiobook_player_provider.g.dart'; diff --git a/lib/features/player/providers/audiobook_player_provider.g.dart b/lib/features/player/providers/audiobook_player_provider.g.dart index 27ed916..c74f23c 100644 --- a/lib/features/player/providers/audiobook_player_provider.g.dart +++ b/lib/features/player/providers/audiobook_player_provider.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'audiobook_player_provider.dart'; +part of 'audiobook_player.dart'; // ************************************************************************** // RiverpodGenerator diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index 9a77793..aad29cd 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -1,6 +1,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart'; +import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; part 'currently_playing_provider.g.dart'; diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart new file mode 100644 index 0000000..b9ccb9f --- /dev/null +++ b/lib/features/player/view/audiobook_player.dart @@ -0,0 +1,242 @@ +import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.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.dart'; +import 'package:whispering_pages/features/player/providers/currently_playing_provider.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); +} + +const double playerMinHeight = 70; +const double playerMaxHeight = 500; +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 itemBeingPlayed = + ref.watch(libraryItemProvider(currentBook.libraryItemId)); + final player = ref.watch(audiobookPlayerProvider); + final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null + ? ref.watch( + coverImageProvider(itemBeingPlayed.valueOrNull!), + ) + : null; + final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null + ? Image.memory( + imageOfItemBeingPlayed!.valueOrNull!, + fit: BoxFit.cover, + ) + : const BookCoverSkeleton(); + + final playPauseController = useAnimationController( + duration: const Duration(milliseconds: 200), + initialValue: 1, + ); + + // add controller to the player state listener + player.onPlayerStateChanged.listen((state) { + if (state == PlayerState.playing) { + playPauseController.reverse(); + } else { + playPauseController.forward(); + } + }); + + final playPauseButton = IconButton( + onPressed: () async { + await player.togglePlayPause(); + }, + icon: AnimatedIcon( + icon: AnimatedIcons.pause_play, + progress: playPauseController, + size: 50, + ), + ); + // player.onPositionChanged.listen((event) { + // currentProgress.value = event.inSeconds.toDouble(); + // }); + + // final progressStream = TimerPositionUpdater( + // getPosition: player.getCurrentPosition, + // interval: const Duration(milliseconds: 500), + // ).positionStream; + // // a debug that will print the current position of the player + // progressStream.listen((event) { + // debugPrint('Current position: ${event.inSeconds}'); + // }); + + // the widget that will be displayed when the player is expanded + const progressBar = PlayerProgressBar(); + + // theme from image + final imageTheme = ref.watch( + themeOfLibraryItemProvider( + itemBeingPlayed.valueOrNull, + brightness: Theme.of(context).brightness, + ), + ); + return Theme( + // get the theme from imageThemeProvider + + data: ThemeData( + colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme, + ), + child: 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 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, + ); + const buttonPlayExpanded = IconButton( + icon: Icon(Icons.pause_circle_filled), + iconSize: 50, + onPressed: onTap, + ); + + return PlayerWhenExpanded( + imgPaddingLeft: paddingLeft, + imgPaddingVertical: paddingVertical, + imageSize: imageSize, + img: imgWidget, + percentageExpandedPlayer: percentageExpandedPlayer, + text: bookTitle, + buttonSkipBackwards: buttonSkipBackwards, + buttonPlayExpanded: playPauseButton, + buttonSkipForward: buttonSkipForward, + progressIndicator: progressBar, + ); + } + + //Miniplayer + final percentageMiniplayer = percentageFromValueInRange( + min: playerMinHeight, + max: playerMaxHeight * miniplayerPercentageDeclaration + + playerMinHeight, + value: height, + ); + + final elementOpacity = 1 - 1 * percentageMiniplayer; + final progressIndicatorHeight = 4 - 4 * percentageMiniplayer; + + return PlayerWhenMinimized( + maxImgSize: maxImgSize, + imgWidget: imgWidget, + elementOpacity: elementOpacity, + playPauseButton: playPauseButton, + progressIndicatorHeight: progressIndicatorHeight, + progressIndicator: progressBar, + ); + }, + ), + ); + } +} + +class PlayerProgressBar extends HookConsumerWidget { + const PlayerProgressBar({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final player = ref.watch(audiobookPlayerProvider); + final playerState = useState(player.state); + // add a listener to the player state + player.onPlayerStateChanged.listen((state) { + playerState.value = state; + }); + return StreamBuilder( + stream: player.onPositionChanged, + builder: (context, snapshot) { + return ProgressBar( + progress: snapshot.data ?? const Duration(seconds: 0), + total: player.book?.duration ?? const Duration(seconds: 0), + onSeek: player.seek, + thumbRadius: 8, + // thumbColor: Theme.of(context).colorScheme.secondary, + thumbGlowColor: Theme.of(context).colorScheme.secondary, + thumbGlowRadius: playerState.value == PlayerState.playing ? 10 : 0, + ); + }, + ); + } +} + +void onTap() {} diff --git a/lib/features/player/view/audioobok_player.dart b/lib/features/player/view/audioobok_player.dart deleted file mode 100644 index abed71b..0000000 --- a/lib/features/player/view/audioobok_player.dart +++ /dev/null @@ -1,248 +0,0 @@ -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/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart new file mode 100644 index 0000000..b4f03ea --- /dev/null +++ b/lib/features/player/view/player_when_expanded.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class PlayerWhenExpanded extends StatelessWidget { + 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.buttonPlayExpanded, + required this.buttonSkipForward, + required this.progressIndicator, + }); + + /// 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 IconButton buttonPlayExpanded; + final IconButton buttonSkipForward; + final Widget progressIndicator; + + @override + Widget build(BuildContext context) { + 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, + ), + ), + ), + ), + 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, + buttonPlayExpanded, + buttonSkipForward, + ], + ), + ), + Flexible(child: progressIndicator), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart new file mode 100644 index 0000000..5c74f17 --- /dev/null +++ b/lib/features/player/view/player_when_minimized.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:miniplayer/miniplayer.dart'; +import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; +import 'package:whispering_pages/features/player/view/audiobook_player.dart'; + +class PlayerWhenMinimized extends HookConsumerWidget { + const PlayerWhenMinimized({ + super.key, + required this.maxImgSize, + required this.imgWidget, + required this.elementOpacity, + required this.playPauseButton, + required this.progressIndicatorHeight, + required this.progressIndicator, + }); + + final double maxImgSize; + final Widget imgWidget; + final double elementOpacity; + final IconButton playPauseButton; + final double progressIndicatorHeight; + final Widget progressIndicator; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final player = ref.watch(audiobookPlayerProvider); + 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: playPauseButton, + ), + ), + ], + ), + ), + SizedBox( + height: progressIndicatorHeight, + child: Opacity( + opacity: elementOpacity, + child: progressIndicator, + ), + ), + ], + ); + } +} diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 7e8df96..d68a9bd 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -1,7 +1,7 @@ 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'; +import 'package:whispering_pages/features/player/view/audiobook_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. diff --git a/pubspec.lock b/pubspec.lock index 00dbdcb..5bf968a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audio_video_progress_bar: + dependency: "direct main" + description: + name: audio_video_progress_bar + sha256: ccc7d7b83d2a16c52d4a7fb332faabd1baa053fb0e4c16815aefd3945ab33b81 + url: "https://pub.dev" + source: hosted + version: "2.0.2" audioplayers: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 957a1a7..cba6259 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 + audio_video_progress_bar: ^2.0.2 audioplayers: ^6.0.0 auto_scroll_text: ^0.0.7 cached_network_image: ^3.3.1