feat: Add audio_video_progress_bar dependency

This commit is contained in:
Dr-Blank 2024-05-15 02:27:05 -04:00
parent 7f5309d10a
commit a1dd0e9d3f
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
12 changed files with 467 additions and 255 deletions

View file

@ -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';

View file

@ -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<void> 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<Duration> 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<Duration?> getDuration() async {
if (_book == null) {
return null;
}
return _book!.tracks.fold<Duration>(
Duration.zero,
(previousValue, element) => previousValue + element.duration,
);
}
@override
Future<Duration?> getCurrentPosition() async {
if (_book == null) {
return null;
}
var currentTrack = _book!.tracks[_currentIndex];
var currentTrackDuration = currentTrack.duration;
var currentTrackPosition = await super.getCurrentPosition();
return currentTrackPosition != null
? currentTrackPosition + currentTrackDuration
: null;
}
}

View file

@ -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';

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audiobook_player_provider.dart';
part of 'audiobook_player.dart';
// **************************************************************************
// RiverpodGenerator

View file

@ -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';

View file

@ -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<double> 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<Duration>(
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() {}

View file

@ -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<double> 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() {}

View file

@ -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),
],
),
),
),
),
],
);
}
}

View file

@ -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,
),
),
],
);
}
}

View file

@ -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.

View file

@ -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:

View file

@ -32,6 +32,7 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
dependencies:
animated_list_plus: ^0.5.2
animated_theme_switcher: ^2.0.10
audio_video_progress_bar: ^2.0.2
audioplayers: ^6.0.0
auto_scroll_text: ^0.0.7
cached_network_image: ^3.3.1