替换miniPlayer

This commit is contained in:
rang 2025-11-13 17:53:23 +08:00
parent eb9b8f3b94
commit e67d045da6
34 changed files with 1777 additions and 1078 deletions

View file

@ -1,271 +1,195 @@
import 'dart:math';
// import 'dart:math';
import 'package:audio_video_progress_bar/audio_video_progress_bar.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:vaani/api/image_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/inverse_lerp.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.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:vaani/api/image_provider.dart';
// import 'package:vaani/api/library_item_provider.dart';
// import 'package:vaani/features/player/providers/audiobook_player.dart';
// import 'package:vaani/features/player/providers/currently_playing_provider.dart';
// import 'package:vaani/features/player/providers/player_form.dart';
// import 'package:vaani/settings/app_settings_provider.dart';
// import 'package:vaani/shared/extensions/inverse_lerp.dart';
// import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
// import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
import 'player_when_expanded.dart';
import 'player_when_minimized.dart';
// import 'player_when_expanded.dart';
// import 'player_when_minimized.dart';
const playerMaxHeightPercentOfScreen = 0.8;
// const playerMaxHeightPercentOfScreen = 0.8;
class AudiobookPlayer extends HookConsumerWidget {
const AudiobookPlayer({super.key});
// class AudiobookPlayer extends HookConsumerWidget {
// const AudiobookPlayer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
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!.id),
)
: null;
final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null
? Image.memory(
imageOfItemBeingPlayed!.valueOrNull!,
fit: BoxFit.cover,
)
: const BookCoverSkeleton();
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final appSettings = ref.watch(appSettingsProvider);
// 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!.id),
// )
// : 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,
);
// final playPauseController = useAnimationController(
// duration: const Duration(milliseconds: 200),
// initialValue: 1,
// );
// add controller to the player state listener
player.playerStateStream.listen((state) {
if (state.playing) {
playPauseController.forward();
} else {
playPauseController.reverse();
}
});
// // add controller to the player state listener
// player.playerStateStream.listen((state) {
// if (state.playing) {
// playPauseController.forward();
// } else {
// playPauseController.reverse();
// }
// });
// theme from image
final imageTheme = ref.watch(
themeOfLibraryItemProvider(
itemBeingPlayed.valueOrNull?.id,
brightness: Theme.of(context).brightness,
highContrast: appSettings.themeSettings.highContrast ||
MediaQuery.of(context).highContrast,
),
);
// // theme from image
// final imageTheme = ref.watch(
// themeOfLibraryItemProvider(
// itemBeingPlayed.valueOrNull?.id,
// brightness: Theme.of(context).brightness,
// highContrast: appSettings.themeSettings.highContrast ||
// MediaQuery.of(context).highContrast,
// ),
// );
// max height of the player is the height of the screen
final playerMaxHeight = MediaQuery.of(context).size.height;
// // max height of the player is the height of the screen
// final playerMaxHeight = MediaQuery.of(context).size.height;
final availWidth = MediaQuery.of(context).size.width;
// final availWidth = MediaQuery.of(context).size.width;
// the image width when the player is expanded
final maxImgSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
// // the image width when the player is expanded
// final maxImgSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
final preferredVolume = appSettings.playerSettings.preferredDefaultVolume;
return Theme(
data: ThemeData(
colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
),
child: Miniplayer(
valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
onDragDown: (percentage) async {
// preferred volume
// set volume to 0 when dragging down
await player
.setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
},
minHeight: playerMinHeight,
// subtract the height of notches and other system UI
maxHeight: playerMaxHeight,
controller: audioBookMiniplayerController,
elevation: 4,
// duration: Duration(seconds: 3),
onDismissed: () {
// add a delay before closing the player
// to allow the user to see the player closing
Future.delayed(const Duration(milliseconds: 300), () {
player.setSourceAudiobook(null);
});
},
curve: Curves.linear,
builder: (height, percentage) {
// at what point should the player switch from miniplayer to expanded player
// also at this point the image should be at its max size and in the center of the player
final miniplayerPercentageDeclaration =
(maxImgSize - playerMinHeight) /
(playerMaxHeight - playerMinHeight);
final bool isFormMiniplayer =
percentage < miniplayerPercentageDeclaration;
// final preferredVolume = appSettings.playerSettings.preferredDefaultVolume;
// return Theme(
// data: ThemeData(
// colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme,
// ),
// child: Miniplayer(
// valueNotifier: ref.watch(playerExpandProgressNotifierProvider),
// onDragDown: (percentage) async {
// // preferred volume
// // set volume to 0 when dragging down
// await player
// .setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
// },
// minHeight: playerMinHeight,
// // subtract the height of notches and other system UI
// maxHeight: playerMaxHeight,
// controller: audioBookMiniplayerController,
// elevation: 4,
// // duration: Duration(seconds: 3),
// onDismissed: () {
// // add a delay before closing the player
// // to allow the user to see the player closing
// Future.delayed(const Duration(milliseconds: 300), () {
// player.setSourceAudiobook(null);
// });
// },
// curve: Curves.linear,
// builder: (height, percentage) {
// // at what point should the player switch from miniplayer to expanded player
// // also at this point the image should be at its max size and in the center of the player
// final miniplayerPercentageDeclaration =
// (maxImgSize - playerMinHeight) /
// (playerMaxHeight - playerMinHeight);
// final bool isFormMiniplayer =
// percentage < miniplayerPercentageDeclaration;
if (!isFormMiniplayer) {
// this calculation needs a refactor
var percentageExpandedPlayer = percentage
.inverseLerp(
miniplayerPercentageDeclaration,
1,
)
.clamp(0.0, 1.0);
// if (!isFormMiniplayer) {
// // this calculation needs a refactor
// var percentageExpandedPlayer = percentage
// .inverseLerp(
// miniplayerPercentageDeclaration,
// 1,
// )
// .clamp(0.0, 1.0);
return PlayerWhenExpanded(
imageSize: maxImgSize,
img: imgWidget,
percentageExpandedPlayer: percentageExpandedPlayer,
playPauseController: playPauseController,
);
}
// return PlayerWhenExpanded(
// imageSize: maxImgSize,
// img: imgWidget,
// percentageExpandedPlayer: percentageExpandedPlayer,
// playPauseController: playPauseController,
// );
// }
//Miniplayer
final percentageMiniplayer = percentage.inverseLerp(
0,
miniplayerPercentageDeclaration,
);
// //Miniplayer
// final percentageMiniplayer = percentage.inverseLerp(
// 0,
// miniplayerPercentageDeclaration,
// );
return PlayerWhenMinimized(
maxImgSize: maxImgSize,
availWidth: availWidth,
imgWidget: imgWidget,
playPauseController: playPauseController,
percentageMiniplayer: percentageMiniplayer,
);
},
),
);
}
}
// return PlayerWhenMinimized(
// maxImgSize: maxImgSize,
// availWidth: availWidth,
// imgWidget: imgWidget,
// playPauseController: playPauseController,
// percentageMiniplayer: percentageMiniplayer,
// );
// },
// ),
// );
// }
// }
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({
super.key,
required this.playPauseController,
this.iconSize = 48.0,
});
// class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
// const AudiobookPlayerPlayPauseButton({
// super.key,
// required this.playPauseController,
// this.iconSize = 48.0,
// });
final double iconSize;
final AnimationController playPauseController;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
// final double iconSize;
// final AnimationController playPauseController;
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final player = ref.watch(audiobookPlayerProvider);
return switch (player.processingState) {
ProcessingState.loading || ProcessingState.buffering => const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
ProcessingState.completed => IconButton(
onPressed: () async {
await player.seekInBook(const Duration(seconds: 0));
await player.play();
},
icon: const Icon(
Icons.replay,
),
),
ProcessingState.ready => IconButton(
onPressed: () async {
await player.togglePlayPause();
},
iconSize: iconSize,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: playPauseController,
),
),
ProcessingState.idle => const SizedBox.shrink(),
};
}
}
// return switch (player.processingState) {
// ProcessingState.loading || ProcessingState.buffering => const Padding(
// padding: EdgeInsets.all(8.0),
// child: CircularProgressIndicator(),
// ),
// ProcessingState.completed => IconButton(
// onPressed: () async {
// await player.seekInBook(const Duration(seconds: 0));
// await player.play();
// },
// icon: const Icon(
// Icons.replay,
// ),
// ),
// ProcessingState.ready => IconButton(
// onPressed: () async {
// await player.togglePlayPause();
// },
// iconSize: iconSize,
// icon: AnimatedIcon(
// icon: AnimatedIcons.play_pause,
// progress: playPauseController,
// ),
// ),
// ProcessingState.idle => const SizedBox.shrink(),
// };
// }
// }
class AudiobookChapterProgressBar extends HookConsumerWidget {
const AudiobookChapterProgressBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final currentChapter = ref.watch(currentPlayingChapterProvider);
final position = useStream(
player.positionStreamInBook,
initialData: const Duration(seconds: 0),
);
final buffered = useStream(
player.bufferedPositionStreamInBook,
initialData: const Duration(seconds: 0),
);
// now find the chapter that corresponds to the current time
// and calculate the progress of the current chapter
final currentChapterProgress = currentChapter == null
? null
: (player.positionInBook - currentChapter.start);
final currentChapterBuffered = currentChapter == null
? null
: (player.bufferedPositionInBook - currentChapter.start);
return ProgressBar(
progress:
currentChapterProgress ?? position.data ?? const Duration(seconds: 0),
total: currentChapter == null
? player.book?.duration ?? const Duration(seconds: 0)
: currentChapter.end - currentChapter.start,
// ! TODO add onSeek
onSeek: (duration) {
player.seekInBook(
duration + (currentChapter?.start ?? const Duration(seconds: 0)),
);
// player.seek(duration);
},
thumbRadius: 8,
buffered:
currentChapterBuffered ?? buffered.data ?? const Duration(seconds: 0),
bufferedBarColor: Theme.of(context).colorScheme.secondary,
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}
class AudiobookProgressBar extends HookConsumerWidget {
const AudiobookProgressBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final position = useStream(
player.slowPositionStreamInBook,
initialData: const Duration(seconds: 0),
);
return ProgressBar(
progress: position.data ?? const Duration(seconds: 0),
total: player.book?.duration ?? const Duration(seconds: 0),
thumbRadius: 8,
bufferedBarColor: Theme.of(context).colorScheme.secondary,
timeLabelType: TimeLabelType.remainingTime,
timeLabelLocation: TimeLabelLocation.below,
);
}
}
// ! TODO remove onTap
void onTap() {}
// // ! TODO remove onTap
// void onTap() {}