mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 19:19:28 +00:00
feat: Add audio_video_progress_bar dependency
This commit is contained in:
parent
7f5309d10a
commit
a1dd0e9d3f
12 changed files with 467 additions and 255 deletions
|
|
@ -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/image_provider.dart';
|
||||||
import 'package:whispering_pages/api/library_item_provider.dart';
|
import 'package:whispering_pages/api/library_item_provider.dart';
|
||||||
import 'package:whispering_pages/constants/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/features/player/providers/audiobook_player.dart';
|
||||||
import 'package:whispering_pages/router/models/library_item_extras.dart';
|
import 'package:whispering_pages/router/models/library_item_extras.dart';
|
||||||
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||||
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
|
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
// the current index of the audio file in the [book]
|
// the current index of the audio file in the [book]
|
||||||
final int _currentIndex = 0;
|
final int _currentIndex = 0;
|
||||||
|
|
||||||
|
// available audio tracks
|
||||||
|
int? get availableTracks => _book?.tracks.length;
|
||||||
|
|
||||||
/// sets the current [AudioTrack] as the source of the player
|
/// sets the current [AudioTrack] as the source of the player
|
||||||
Future<void> setSourceAudioBook(BookExpanded? book) async {
|
Future<void> setSourceAudioBook(BookExpanded? book) async {
|
||||||
// if the book is null, stop the player
|
// if the book is null, stop the player
|
||||||
|
|
@ -50,7 +53,6 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
var url = '$baseUrl${track.contentUrl}?token=$token';
|
var url = '$baseUrl${track.contentUrl}?token=$token';
|
||||||
await setSourceUrl(
|
await setSourceUrl(
|
||||||
url,
|
url,
|
||||||
// '${track.contentUrl}?token=$token',
|
|
||||||
mimeType: track.mimeType,
|
mimeType: track.mimeType,
|
||||||
);
|
);
|
||||||
_book = book;
|
_book = book;
|
||||||
|
|
@ -81,5 +83,37 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
}
|
}
|
||||||
return super.resume();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:whispering_pages/api/api_provider.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';
|
part 'audiobook_player_provider.g.dart';
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'audiobook_player_provider.dart';
|
part of 'audiobook_player.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.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';
|
part 'currently_playing_provider.g.dart';
|
||||||
|
|
||||||
|
|
|
||||||
242
lib/features/player/view/audiobook_player.dart
Normal file
242
lib/features/player/view/audiobook_player.dart
Normal 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() {}
|
||||||
|
|
@ -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() {}
|
|
||||||
79
lib/features/player/view/player_when_expanded.dart
Normal file
79
lib/features/player/view/player_when_expanded.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
lib/features/player/view/player_when_minimized.dart
Normal file
96
lib/features/player/view/player_when_minimized.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:whispering_pages/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
|
/// Builds the "shell" for the app by building a Scaffold with a
|
||||||
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
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:
|
audioplayers:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
|
||||||
dependencies:
|
dependencies:
|
||||||
animated_list_plus: ^0.5.2
|
animated_list_plus: ^0.5.2
|
||||||
animated_theme_switcher: ^2.0.10
|
animated_theme_switcher: ^2.0.10
|
||||||
|
audio_video_progress_bar: ^2.0.2
|
||||||
audioplayers: ^6.0.0
|
audioplayers: ^6.0.0
|
||||||
auto_scroll_text: ^0.0.7
|
auto_scroll_text: ^0.0.7
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue