mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-27 05:19:31 +00:00
migrate to just audio
This commit is contained in:
parent
a1dd0e9d3f
commit
01b3dead49
22 changed files with 1062 additions and 340 deletions
|
|
@ -12,8 +12,8 @@ import 'package:whispering_pages/constants/hero_tag_conventions.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';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart';
|
||||
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
|
||||
|
||||
import '../../../shared/widgets/expandable_description.dart';
|
||||
import 'library_item_sliver_app_bar.dart';
|
||||
|
|
@ -219,7 +219,7 @@ class LibraryItemMetadata extends StatelessWidget {
|
|||
return null;
|
||||
}
|
||||
final codec = book.audioFiles.first.codec.toUpperCase();
|
||||
final bitrate = book.audioFiles.first.bitRate;
|
||||
// final bitrate = book.audioFiles.first.bitRate;
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
|
|
@ -277,85 +277,83 @@ class LibraryItemActions extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.read(audiobookPlayerProvider);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
// play/resume button the same width as image
|
||||
LayoutBuilder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
// play/resume button the same width as image
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: calculateWidth(context, constraints),
|
||||
// a boxy button with icon and text but little rounded corner
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
// play the book
|
||||
debugPrint('Pressed play/resume button');
|
||||
// set the book to the player if not already set
|
||||
if (player.book != book) {
|
||||
debugPrint('Setting the book ${book.libraryItemId}');
|
||||
await player.setSourceAudioBook(book);
|
||||
ref
|
||||
.read(audiobookPlayerProvider.notifier)
|
||||
.notifyListeners();
|
||||
}
|
||||
// toggle play/pause
|
||||
player.togglePlayPause();
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: const Text('Play/Resume'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: calculateWidth(context, constraints),
|
||||
// a boxy button with icon and text but little rounded corner
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
// play the book
|
||||
debugPrint('Pressed play/resume button');
|
||||
// set the book to the player if not already set
|
||||
if (player.book != book) {
|
||||
debugPrint('Setting the book ${book.libraryItemId}');
|
||||
await player.setSourceAudioBook(book);
|
||||
ref
|
||||
.read(audiobookPlayerProvider.notifier)
|
||||
.notifyListeners();
|
||||
}
|
||||
// toggle play/pause
|
||||
player.togglePlayPause();
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: const Text('Play/Resume'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
width: constraints.maxWidth * 0.6,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// read list button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.playlist_add_rounded,
|
||||
),
|
||||
),
|
||||
),
|
||||
// share button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
),
|
||||
// download button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.download_rounded,
|
||||
),
|
||||
),
|
||||
// more button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.more_vert_rounded,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth * 0.6,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// read list button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.playlist_add_rounded,
|
||||
),
|
||||
),
|
||||
// share button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
),
|
||||
// download button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.download_rounded,
|
||||
),
|
||||
),
|
||||
// more button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.more_vert_rounded,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
/// this is needed as audiobook can be a list of audio files instead of a single file
|
||||
library;
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:just_audio_background/just_audio_background.dart';
|
||||
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.playerId}) : super() {
|
||||
AudiobookPlayer(this.token, this.baseUrl) : super() {
|
||||
// set the source of the player to the first track in the book
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +30,7 @@ class AudiobookPlayer extends AudioPlayer {
|
|||
final Uri baseUrl;
|
||||
|
||||
// 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;
|
||||
|
|
@ -46,15 +48,32 @@ class AudiobookPlayer extends AudioPlayer {
|
|||
// if the book is the same, do nothing
|
||||
return;
|
||||
}
|
||||
// first stop the player
|
||||
// first stop the player and clear the source
|
||||
await stop();
|
||||
|
||||
var track = book.tracks[_currentIndex];
|
||||
var url = '$baseUrl${track.contentUrl}?token=$token';
|
||||
await setSourceUrl(
|
||||
url,
|
||||
mimeType: track.mimeType,
|
||||
);
|
||||
await setAudioSource(
|
||||
ConcatenatingAudioSource(
|
||||
useLazyPreparation: true,
|
||||
children: book.tracks.map((track) {
|
||||
return AudioSource.uri(
|
||||
Uri.parse('$baseUrl${track.contentUrl}?token=$token'),
|
||||
tag: MediaItem(
|
||||
// Specify a unique ID for each media item:
|
||||
id: book.libraryItemId + track.index.toString(),
|
||||
// Metadata to display in the notification:
|
||||
album: book.metadata.title,
|
||||
title: book.metadata.title ?? track.title,
|
||||
artUri: Uri.parse(
|
||||
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
).catchError((error) {
|
||||
debugPrint('Error: $error');
|
||||
});
|
||||
|
||||
_book = book;
|
||||
}
|
||||
|
||||
|
|
@ -64,56 +83,37 @@ class AudiobookPlayer extends AudioPlayer {
|
|||
if (_book == null) {
|
||||
throw StateError('No book is set');
|
||||
}
|
||||
return switch (state) {
|
||||
PlayerState.playing => pause(),
|
||||
PlayerState.paused ||
|
||||
PlayerState.stopped ||
|
||||
PlayerState.completed =>
|
||||
resume(),
|
||||
// do nothing if the player is disposed
|
||||
PlayerState.disposed => throw StateError('Player is disposed'),
|
||||
|
||||
// ! refactor this
|
||||
return switch (playerState) {
|
||||
PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(),
|
||||
};
|
||||
}
|
||||
|
||||
/// override resume to set the source if the book is not set
|
||||
@override
|
||||
Future<void> resume() async {
|
||||
if (_book == null) {
|
||||
throw StateError('No book is set');
|
||||
}
|
||||
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?> 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;
|
||||
}
|
||||
// @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;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ class AudiobookPlayer extends _$AudiobookPlayer {
|
|||
abp.AudiobookPlayer build() {
|
||||
final api = ref.watch(authenticatedApiProvider);
|
||||
final player =
|
||||
abp.AudiobookPlayer(api.token!, api.baseUrl, playerId: playerId);
|
||||
abp.AudiobookPlayer(api.token!, api.baseUrl);
|
||||
|
||||
ref.onDispose(player.dispose);
|
||||
|
||||
// bind notify listeners to the player
|
||||
player.onPlayerStateChanged.listen((_) {
|
||||
player.playerStateStream.listen((_) {
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
|
|
|
|||
67
lib/features/player/providers/player_form.dart
Normal file
67
lib/features/player/providers/player_form.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// this provider is used to manage the player form state
|
||||
// it will inform about the percentage of the player expanded
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:miniplayer/miniplayer.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'player_form.g.dart';
|
||||
|
||||
const double playerMinHeight = 70;
|
||||
const miniplayerPercentageDeclaration = 0.2;
|
||||
|
||||
extension on Ref {
|
||||
// We can move the previous logic to a Ref extension.
|
||||
// This enables reusing the logic between providers
|
||||
T disposeAndListenChangeNotifier<T extends ChangeNotifier>(T notifier) {
|
||||
onDispose(notifier.dispose);
|
||||
notifier.addListener(notifyListeners);
|
||||
// We return the notifier to ease the usage a bit
|
||||
return notifier;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Raw<ValueNotifier<double>> playerExpandProgressNotifier(
|
||||
PlayerExpandProgressNotifierRef ref,
|
||||
) {
|
||||
final ValueNotifier<double> playerExpandProgress =
|
||||
ValueNotifier(playerMinHeight);
|
||||
|
||||
return ref.disposeAndListenChangeNotifier(playerExpandProgress);
|
||||
}
|
||||
|
||||
// @Riverpod(keepAlive: true)
|
||||
// Raw<ValueNotifier<double>> dragDownPercentageNotifier(
|
||||
// DragDownPercentageNotifierRef ref,
|
||||
// ) {
|
||||
// final ValueNotifier<double> notifier = ValueNotifier(0);
|
||||
|
||||
// return ref.disposeAndListenChangeNotifier(notifier);
|
||||
// }
|
||||
|
||||
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
|
||||
@Riverpod(keepAlive: true)
|
||||
double playerHeight(
|
||||
PlayerHeightRef ref,
|
||||
) {
|
||||
final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider);
|
||||
|
||||
// on change of the playerExpandProgress invalidate
|
||||
playerExpandProgress.addListener(() {
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
|
||||
// listen to the playerExpandProgressNotifier and return the value
|
||||
return playerExpandProgress.value;
|
||||
}
|
||||
|
||||
// a final MiniplayerController controller = MiniplayerController();
|
||||
@Riverpod(keepAlive: true)
|
||||
MiniplayerController miniplayerController(
|
||||
MiniplayerControllerRef ref,
|
||||
) {
|
||||
return MiniplayerController();
|
||||
}
|
||||
58
lib/features/player/providers/player_form.g.dart
Normal file
58
lib/features/player/providers/player_form.g.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'player_form.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$playerExpandProgressNotifierHash() =>
|
||||
r'e4817361b9a311b61ca23e51082ed11b0a1120ab';
|
||||
|
||||
/// See also [playerExpandProgressNotifier].
|
||||
@ProviderFor(playerExpandProgressNotifier)
|
||||
final playerExpandProgressNotifierProvider =
|
||||
Provider<Raw<ValueNotifier<double>>>.internal(
|
||||
playerExpandProgressNotifier,
|
||||
name: r'playerExpandProgressNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$playerExpandProgressNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PlayerExpandProgressNotifierRef
|
||||
= ProviderRef<Raw<ValueNotifier<double>>>;
|
||||
String _$playerHeightHash() => r'26dbcb180d494575488d700bd5bdb58c02c224a9';
|
||||
|
||||
/// See also [playerHeight].
|
||||
@ProviderFor(playerHeight)
|
||||
final playerHeightProvider = Provider<double>.internal(
|
||||
playerHeight,
|
||||
name: r'playerHeightProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$playerHeightHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PlayerHeightRef = ProviderRef<double>;
|
||||
String _$miniplayerControllerHash() =>
|
||||
r'489579a18f4e08793de08a4828172bd924768301';
|
||||
|
||||
/// See also [miniplayerController].
|
||||
@ProviderFor(miniplayerController)
|
||||
final miniplayerControllerProvider = Provider<MiniplayerController>.internal(
|
||||
miniplayerController,
|
||||
name: r'miniplayerControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$miniplayerControllerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef MiniplayerControllerRef = ProviderRef<MiniplayerController>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
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:just_audio/just_audio.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/features/player/providers/player_form.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart';
|
||||
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
|
||||
|
||||
|
|
@ -26,13 +27,7 @@ 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});
|
||||
|
|
@ -64,39 +59,19 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
);
|
||||
|
||||
// add controller to the player state listener
|
||||
player.onPlayerStateChanged.listen((state) {
|
||||
if (state == PlayerState.playing) {
|
||||
playPauseController.reverse();
|
||||
} else {
|
||||
player.playerStateStream.listen((state) {
|
||||
if (state.playing) {
|
||||
playPauseController.forward();
|
||||
} else {
|
||||
playPauseController.reverse();
|
||||
}
|
||||
});
|
||||
|
||||
final playPauseButton = IconButton(
|
||||
onPressed: () async {
|
||||
await player.togglePlayPause();
|
||||
},
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.pause_play,
|
||||
progress: playPauseController,
|
||||
size: 50,
|
||||
),
|
||||
final playPauseButton = AudiobookPlayerPlayPauseButton(
|
||||
playPauseController: playPauseController,
|
||||
);
|
||||
// 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();
|
||||
const progressBar = AudiobookTotalProgressBar();
|
||||
|
||||
// theme from image
|
||||
final imageTheme = ref.watch(
|
||||
|
|
@ -105,30 +80,38 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
brightness: Theme.of(context).brightness,
|
||||
),
|
||||
);
|
||||
return Theme(
|
||||
// get the theme from imageThemeProvider
|
||||
|
||||
// max height of the player is the height of the screen
|
||||
final playerMaxHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
|
||||
),
|
||||
child: Miniplayer(
|
||||
valueNotifier: playerExpandProgress,
|
||||
valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
|
||||
minHeight: playerMinHeight,
|
||||
maxHeight: playerMaxHeight,
|
||||
controller: controller,
|
||||
controller: ref.watch(miniplayerControllerProvider),
|
||||
elevation: 4,
|
||||
onDismissed: () {
|
||||
player.setSourceAudioBook(null);
|
||||
},
|
||||
curve: Curves.easeOut,
|
||||
builder: (height, percentage) {
|
||||
// return SafeArea(
|
||||
// child: Text(
|
||||
// 'percentage: ${percentage.toStringAsFixed(2)}, height: ${height.toStringAsFixed(2)}',
|
||||
// ),
|
||||
// );
|
||||
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(
|
||||
|
|
@ -153,7 +136,7 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
percentage: percentageExpandedPlayer,
|
||||
) /
|
||||
2;
|
||||
|
||||
|
||||
const buttonSkipForward = IconButton(
|
||||
icon: Icon(Icons.forward_30),
|
||||
iconSize: 33,
|
||||
|
|
@ -164,12 +147,6 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
iconSize: 33,
|
||||
onPressed: onTap,
|
||||
);
|
||||
const buttonPlayExpanded = IconButton(
|
||||
icon: Icon(Icons.pause_circle_filled),
|
||||
iconSize: 50,
|
||||
onPressed: onTap,
|
||||
);
|
||||
|
||||
return PlayerWhenExpanded(
|
||||
imgPaddingLeft: paddingLeft,
|
||||
imgPaddingVertical: paddingVertical,
|
||||
|
|
@ -178,12 +155,12 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
percentageExpandedPlayer: percentageExpandedPlayer,
|
||||
text: bookTitle,
|
||||
buttonSkipBackwards: buttonSkipBackwards,
|
||||
buttonPlayExpanded: playPauseButton,
|
||||
playPauseButton: playPauseButton,
|
||||
buttonSkipForward: buttonSkipForward,
|
||||
progressIndicator: progressBar,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//Miniplayer
|
||||
final percentageMiniplayer = percentageFromValueInRange(
|
||||
min: playerMinHeight,
|
||||
|
|
@ -191,16 +168,14 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
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,
|
||||
);
|
||||
},
|
||||
|
|
@ -209,34 +184,102 @@ class AudiobookPlayer extends HookConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class PlayerProgressBar extends HookConsumerWidget {
|
||||
const PlayerProgressBar({
|
||||
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
||||
const AudiobookPlayerPlayPauseButton({
|
||||
super.key,
|
||||
required this.playPauseController,
|
||||
});
|
||||
|
||||
final AnimationController playPauseController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(audiobookPlayerProvider);
|
||||
return switch (player.processingState) {
|
||||
ProcessingState.loading ||
|
||||
ProcessingState.buffering =>
|
||||
const CircularProgressIndicator(),
|
||||
ProcessingState.completed => IconButton(
|
||||
onPressed: () async {
|
||||
await player.seek(const Duration(seconds: 0));
|
||||
await player.play();
|
||||
},
|
||||
icon: const Icon(Icons.replay),
|
||||
),
|
||||
ProcessingState.ready => IconButton(
|
||||
onPressed: () async {
|
||||
await player.togglePlayPause();
|
||||
},
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: playPauseController,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
ProcessingState.idle => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A progress bar that shows the total progress of the audiobook
|
||||
///
|
||||
/// for chapter progress, use [AudiobookChapterProgressBar]
|
||||
class AudiobookTotalProgressBar extends HookConsumerWidget {
|
||||
const AudiobookTotalProgressBar({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(audiobookPlayerProvider);
|
||||
final playerState = useState(player.state);
|
||||
// final playerState = useState(player.processingState);
|
||||
// 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,
|
||||
// player.processingStateStream.listen((state) {
|
||||
// playerState.value = state;
|
||||
// });
|
||||
return StreamBuilder(
|
||||
stream: player.currentIndexStream,
|
||||
builder: (context, currentTrackIndex) {
|
||||
return StreamBuilder(
|
||||
stream: player.positionStream,
|
||||
builder: (context, progress) {
|
||||
// totalProgress is the sum of the duration of all the tracks before the current track + the current track position
|
||||
final totalProgress =
|
||||
player.book?.tracks.sublist(0, currentTrackIndex.data).fold(
|
||||
const Duration(seconds: 0),
|
||||
(previousValue, element) =>
|
||||
previousValue + element.duration,
|
||||
) ??
|
||||
const Duration(seconds: 0) +
|
||||
(progress.data ?? const Duration(seconds: 0));
|
||||
|
||||
return StreamBuilder(
|
||||
stream: player.bufferedPositionStream,
|
||||
builder: (context, buffered) {
|
||||
final totalBuffered =
|
||||
player.book?.tracks.sublist(0, currentTrackIndex.data).fold(
|
||||
const Duration(seconds: 0),
|
||||
(previousValue, element) =>
|
||||
previousValue + element.duration,
|
||||
) ??
|
||||
const Duration(seconds: 0) +
|
||||
(buffered.data ?? const Duration(seconds: 0));
|
||||
return ProgressBar(
|
||||
progress: totalProgress,
|
||||
total: player.book?.duration ?? const Duration(seconds: 0),
|
||||
onSeek: player.seek,
|
||||
thumbRadius: 8,
|
||||
buffered: totalBuffered,
|
||||
bufferedBarColor: Theme.of(context).colorScheme.secondary,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO remove onTap
|
||||
void onTap() {}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class PlayerWhenExpanded extends StatelessWidget {
|
|||
required this.percentageExpandedPlayer,
|
||||
required this.text,
|
||||
required this.buttonSkipBackwards,
|
||||
required this.buttonPlayExpanded,
|
||||
required this.playPauseButton,
|
||||
required this.buttonSkipForward,
|
||||
required this.progressIndicator,
|
||||
});
|
||||
|
|
@ -23,7 +23,7 @@ class PlayerWhenExpanded extends StatelessWidget {
|
|||
final double percentageExpandedPlayer;
|
||||
final Text text;
|
||||
final IconButton buttonSkipBackwards;
|
||||
final IconButton buttonPlayExpanded;
|
||||
final Widget playPauseButton;
|
||||
final IconButton buttonSkipForward;
|
||||
final Widget progressIndicator;
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ class PlayerWhenExpanded extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
buttonSkipBackwards,
|
||||
buttonPlayExpanded,
|
||||
playPauseButton,
|
||||
buttonSkipForward,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
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';
|
||||
import 'package:whispering_pages/features/player/providers/player_form.dart';
|
||||
|
||||
class PlayerWhenMinimized extends HookConsumerWidget {
|
||||
const PlayerWhenMinimized({
|
||||
|
|
@ -11,20 +10,19 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
|||
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 playPauseButton;
|
||||
final Widget progressIndicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(audiobookPlayerProvider);
|
||||
final controller = ref.watch(miniplayerControllerProvider);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -67,14 +65,14 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
controller.animateToHeight(state: PanelState.MAX);
|
||||
},
|
||||
),
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.fullscreen),
|
||||
// onPressed: () {
|
||||
// controller.animateToHeight(state: PanelState.MAX);
|
||||
// },
|
||||
// ),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 3),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Opacity(
|
||||
opacity: elementOpacity,
|
||||
child: playPauseButton,
|
||||
|
|
@ -83,13 +81,13 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: progressIndicatorHeight,
|
||||
child: Opacity(
|
||||
opacity: elementOpacity,
|
||||
child: progressIndicator,
|
||||
),
|
||||
),
|
||||
// SizedBox(
|
||||
// height: progressIndicatorHeight,
|
||||
// child: Opacity(
|
||||
// opacity: elementOpacity,
|
||||
// child: progressIndicator,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue