mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-06 17:39:34 +00:00
feat: Add miniplayer
This commit is contained in:
parent
610d9a2aa0
commit
7f5309d10a
25 changed files with 355 additions and 29 deletions
|
|
@ -8,15 +8,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
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/extensions/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/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/widgets/shelves/book_shelf.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart';
|
||||
|
||||
import '../../../widgets/expandable_description.dart';
|
||||
import '../../../widgets/library_item_sliver_app_bar.dart';
|
||||
import '../../../shared/widgets/expandable_description.dart';
|
||||
import 'library_item_sliver_app_bar.dart';
|
||||
|
||||
class LibraryItemPage extends HookConsumerWidget {
|
||||
const LibraryItemPage({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class LibraryItemSliverAppBar extends StatelessWidget {
|
||||
const LibraryItemSliverAppBar({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
floating: true,
|
||||
primary: true,
|
||||
snap: true,
|
||||
actions: [
|
||||
// cast button
|
||||
IconButton(onPressed: () {}, icon: const Icon(Icons.cast)),
|
||||
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
219
lib/features/onboarding/view/onboarding_single_page.dart
Normal file
219
lib/features/onboarding/view/onboarding_single_page.dart
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:whispering_pages/api/api_provider.dart';
|
||||
import 'package:whispering_pages/api/authenticated_user_provider.dart';
|
||||
import 'package:whispering_pages/api/server_provider.dart';
|
||||
import 'package:whispering_pages/router/router.dart';
|
||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||
import 'package:whispering_pages/settings/models/models.dart' as model;
|
||||
import 'package:whispering_pages/shared/widgets/add_new_server.dart';
|
||||
import 'package:whispering_pages/features/onboarding/view/user_login.dart';
|
||||
|
||||
class OnboardingSinglePage extends HookConsumerWidget {
|
||||
const OnboardingSinglePage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
final serverUriController = useTextEditingController(
|
||||
text: apiSettings.activeServer?.serverUrl.toString() ?? '',
|
||||
);
|
||||
final api = ref
|
||||
.watch(audiobookshelfApiProvider(Uri.https(serverUriController.text)));
|
||||
|
||||
final isUserLoginAvailable = useState(apiSettings.activeServer != null);
|
||||
|
||||
final usernameController = useTextEditingController();
|
||||
final passwordController = useTextEditingController();
|
||||
|
||||
void addServer() {
|
||||
var newServer = serverUriController.text.isEmpty
|
||||
? null
|
||||
: model.AudiobookShelfServer(
|
||||
serverUrl: Uri.parse(serverUriController.text),
|
||||
);
|
||||
try {
|
||||
// add the server to the list of servers
|
||||
if (newServer != null) {
|
||||
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
||||
newServer,
|
||||
);
|
||||
}
|
||||
// else remove the server from the list of servers
|
||||
else if (apiSettings.activeServer != null) {
|
||||
ref
|
||||
.read(audiobookShelfServerProvider.notifier)
|
||||
.removeServer(apiSettings.activeServer!);
|
||||
}
|
||||
} on ServerAlreadyExistsException catch (e) {
|
||||
newServer = e.server;
|
||||
} finally {
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(
|
||||
activeServer: newServer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Login to the server and save the user
|
||||
Future<void> loginAndSave() async {
|
||||
final username = usernameController.text;
|
||||
final password = passwordController.text;
|
||||
final success = await api.login(username: username, password: password);
|
||||
if (success != null) {
|
||||
// save the server
|
||||
addServer();
|
||||
var authenticatedUser = model.AuthenticatedUser(
|
||||
server: model.AudiobookShelfServer(
|
||||
serverUrl: Uri.parse(serverUriController.text),
|
||||
),
|
||||
id: success.user.id,
|
||||
password: password,
|
||||
username: username,
|
||||
authToken: api.token!,
|
||||
);
|
||||
// add the user to the list of users
|
||||
ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser);
|
||||
|
||||
// set the active user
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(
|
||||
activeUser: authenticatedUser,
|
||||
),
|
||||
);
|
||||
|
||||
// redirect to the library page
|
||||
GoRouter.of(context).goNamed(Routes.home);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Login failed. Please check your credentials.'),
|
||||
),
|
||||
);
|
||||
// give focus back to the username field
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Welcome to Whispering Pages',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox.square(
|
||||
dimension: 16.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Please enter the URL of your AudiobookShelf Server',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AddNewServer(
|
||||
controller: serverUriController,
|
||||
allowEmpty: true,
|
||||
onPressed: () {
|
||||
isUserLoginAvailable.value =
|
||||
serverUriController.text.isNotEmpty;
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: 500.ms,
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.3),
|
||||
end: const Offset(0, 0),
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: isUserLoginAvailable.value
|
||||
? UserLogin(
|
||||
passwordController: passwordController,
|
||||
usernameController: usernameController,
|
||||
onPressed: loginAndSave,
|
||||
)
|
||||
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
|
||||
: const RedirectToABS().animate().fadeIn().slideY(
|
||||
curve: Curves.easeInOut,
|
||||
duration: 500.ms,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RedirectToABS extends StatelessWidget {
|
||||
const RedirectToABS({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// a simple text with hyper link to only the "click here" part
|
||||
const Text('Do not have a server? '),
|
||||
// a simple text with hyper link to the github page
|
||||
TextButton(
|
||||
autofocus: false,
|
||||
isSemanticButton: false,
|
||||
style: ButtonStyle(
|
||||
elevation: MaterialStateProperty.all(0),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
// open the github page
|
||||
// ignore: avoid_print
|
||||
print('Opening the github page');
|
||||
await _launchUrl(
|
||||
Uri.parse(
|
||||
'https://www.audiobookshelf.org',
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Click here'),
|
||||
),
|
||||
const Text(' to know how to setup a server.'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _launchUrl(Uri url) async {
|
||||
if (!await launchUrl(
|
||||
url,
|
||||
mode: LaunchMode.platformDefault,
|
||||
webOnlyWindowName: '_blank',
|
||||
)) {
|
||||
// throw Exception('Could not launch $url');
|
||||
debugPrint('Could not launch $url');
|
||||
}
|
||||
}
|
||||
122
lib/features/onboarding/view/user_login.dart
Normal file
122
lib/features/onboarding/view/user_login.dart
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:whispering_pages/hacks/fix_autofill_losing_focus.dart';
|
||||
|
||||
class UserLogin extends HookConsumerWidget {
|
||||
UserLogin({
|
||||
super.key,
|
||||
this.usernameController,
|
||||
this.passwordController,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
TextEditingController? usernameController;
|
||||
TextEditingController? passwordController;
|
||||
final void Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
usernameController ??= useTextEditingController();
|
||||
passwordController ??= useTextEditingController();
|
||||
final isPasswordVisibleAnimationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
|
||||
var isPasswordVisible = useState(false);
|
||||
|
||||
// forward animation when the password visibility changes
|
||||
useEffect(
|
||||
() {
|
||||
if (isPasswordVisible.value) {
|
||||
isPasswordVisibleAnimationController.forward();
|
||||
} else {
|
||||
isPasswordVisibleAnimationController.reverse();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[isPasswordVisible.value],
|
||||
);
|
||||
|
||||
return Center(
|
||||
child: InactiveFocusScopeObserver(
|
||||
child: AutofillGroup(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: usernameController,
|
||||
autofocus: true,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: !isPasswordVisible.value,
|
||||
onFieldSubmitted: onPressed != null
|
||||
? (_) {
|
||||
onPressed!();
|
||||
}
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/Animation - 1714930099660.json',
|
||||
controller: isPasswordVisibleAnimationController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 45,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ 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() {
|
||||
AudiobookPlayer(this.token, this.baseUrl, {super.playerId}) : super() {
|
||||
// set the source of the player to the first track in the book
|
||||
}
|
||||
|
||||
|
|
@ -31,12 +31,20 @@ class AudiobookPlayer extends AudioPlayer {
|
|||
final int _currentIndex = 0;
|
||||
|
||||
/// 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 (book == null) {
|
||||
_book = null;
|
||||
return stop();
|
||||
}
|
||||
|
||||
// see if the book is the same as the current book
|
||||
if (_book == book) {
|
||||
// if the book is the same, do nothing
|
||||
return;
|
||||
}
|
||||
// first stop the player
|
||||
await stop();
|
||||
|
||||
var track = book.tracks[_currentIndex];
|
||||
var url = '$baseUrl${track.contentUrl}?token=$token';
|
||||
|
|
@ -64,9 +72,14 @@ class AudiobookPlayer extends AudioPlayer {
|
|||
PlayerState.disposed => throw StateError('Player is disposed'),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
||||
void main(List<String> args) {
|
||||
final AudiobookPlayer abPlayer = AudiobookPlayer('', Uri.parse(''));
|
||||
print(abPlayer.resume());
|
||||
}
|
||||
|
|
@ -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/audiobook_payer.dart' as abp;
|
||||
import 'package:whispering_pages/features/player/core/audiobook_payer.dart' as abp;
|
||||
|
||||
part 'audiobook_player_provider.g.dart';
|
||||
|
||||
|
|
@ -16,15 +16,23 @@ part 'audiobook_player_provider.g.dart';
|
|||
// return player;
|
||||
// }
|
||||
|
||||
const playerId = 'audiobook_player';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AudiobookPlayer extends _$AudiobookPlayer {
|
||||
@override
|
||||
abp.AudiobookPlayer build() {
|
||||
final api = ref.watch(authenticatedApiProvider);
|
||||
final player = abp.AudiobookPlayer(api.token!, api.baseUrl);
|
||||
final player =
|
||||
abp.AudiobookPlayer(api.token!, api.baseUrl, playerId: playerId);
|
||||
|
||||
ref.onDispose(player.dispose);
|
||||
|
||||
// bind notify listeners to the player
|
||||
player.onPlayerStateChanged.listen((_) {
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'audiobook_player_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$audiobookPlayerHash() => r'8cbadcb264382300e63b3dbaf167a3bea1638a6e';
|
||||
String _$audiobookPlayerHash() => r'a636d5e8e73dc6bbf7b3f47f83884bb3af3b9370';
|
||||
|
||||
/// See also [AudiobookPlayer].
|
||||
@ProviderFor(AudiobookPlayer)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart';
|
||||
|
||||
part 'currently_playing_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) {
|
||||
final player = ref.watch(audiobookPlayerProvider);
|
||||
return player.book;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'currently_playing_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$currentlyPlayingBookHash() =>
|
||||
r'c777ea8b463d8441a0da5e08b4c41b501ce68aad';
|
||||
|
||||
/// See also [currentlyPlayingBook].
|
||||
@ProviderFor(currentlyPlayingBook)
|
||||
final currentlyPlayingBookProvider =
|
||||
AutoDisposeProvider<BookExpanded?>.internal(
|
||||
currentlyPlayingBook,
|
||||
name: r'currentlyPlayingBookProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$currentlyPlayingBookHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef<BookExpanded?>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
248
lib/features/player/view/audioobok_player.dart
Normal file
248
lib/features/player/view/audioobok_player.dart
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
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() {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue