feat: Improved floating player bar

This commit is contained in:
PartyDonut 2025-07-31 16:24:41 +02:00
parent 82e09b3e0c
commit 013722fc96
8 changed files with 338 additions and 210 deletions

View file

@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/items/trick_play_model.dart';
@ -115,6 +116,15 @@ class DirectPlaybackModel extends PlaybackModel {
return null; return null;
} }
@override
DirectPlaybackModel? updateUserData(UserData userData) {
return copyWith(
item: item.copyWith(
userData: userData,
),
);
}
@override @override
String toString() => 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)'; String toString() => 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)';

View file

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/items/trick_play_model.dart';
@ -96,6 +97,15 @@ class OfflinePlaybackModel extends PlaybackModel {
return false; return false;
} }
@override
OfflinePlaybackModel? updateUserData(UserData userData) {
return copyWith(
item: item.copyWith(
userData: userData,
),
);
}
@override @override
String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)'; String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)';

View file

@ -12,6 +12,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
@ -84,6 +85,8 @@ class PlaybackModel {
Future<Duration>? startDuration() async => item.userData.playBackPosition; Future<Duration>? startDuration() async => item.userData.playBackPosition;
PlaybackModel? updateUserData(UserData userData) => throw UnimplementedError();
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError(); Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError(); Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
Future<PlaybackModel>? setQualityOption(Map<Bitrate, bool> map) => throw UnimplementedError(); Future<PlaybackModel>? setQualityOption(Map<Bitrate, bool> map) => throw UnimplementedError();

View file

@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/items/trick_play_model.dart';
@ -51,7 +52,7 @@ class TranscodePlaybackModel extends PlaybackModel {
} }
@override @override
Future<TranscodePlaybackModel>? setQualityOption(Map<Bitrate, bool> map) async => copyWith(bitRateOptions: map); Future<TranscodePlaybackModel>? setQualityOption(Map<Bitrate, bool> map) async => copyWith(bitRateOptions: map);
@override @override
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async { Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
@ -114,6 +115,15 @@ class TranscodePlaybackModel extends PlaybackModel {
return this; return this;
} }
@override
TranscodePlaybackModel? updateUserData(UserData userData) {
return copyWith(
item: item.copyWith(
userData: userData,
),
);
}
@override @override
String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)'; String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)';

View file

@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:overflow_view/overflow_view.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
@ -15,10 +16,16 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
const videoPlayerHeroTag = "HeroPlayer"; const videoPlayerHeroTag = "HeroPlayer";
const floatingPlayerHeight = 70.0; double floatingPlayerHeight(BuildContext context) => switch (AdaptiveLayout.viewSizeOf(context)) {
ViewSize.phone => 75,
ViewSize.tablet => 85,
ViewSize.desktop => 95,
};
class FloatingPlayerBar extends ConsumerStatefulWidget { class FloatingPlayerBar extends ConsumerStatefulWidget {
const FloatingPlayerBar({super.key}); const FloatingPlayerBar({super.key});
@ -29,6 +36,8 @@ class FloatingPlayerBar extends ConsumerStatefulWidget {
class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> { class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
bool showExpandButton = false; bool showExpandButton = false;
bool changingSliderValue = false;
Duration lastPosition = Duration.zero;
Future<void> openFullScreenPlayer() async { Future<void> openFullScreenPlayer() async {
setState(() => showExpandButton = false); setState(() => showExpandButton = false);
@ -58,155 +67,239 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final playbackInfo = ref.watch(mediaPlaybackProvider); final playbackInfo = ref.watch(mediaPlaybackProvider);
final player = ref.watch(videoPlayerProvider); final player = ref.watch(videoPlayerProvider);
final playbackModel = ref.watch(playBackModel.select((value) => value?.item)); final item = ref.watch(playBackModel.select((value) => value?.item));
final progress = playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds; if (!changingSliderValue) {
return Dismissible( lastPosition = playbackInfo.position;
key: const Key("CurrentlyPlayingBar"), }
confirmDismiss: (direction) async {
if (direction == DismissDirection.up) { var isFavourite = item?.userData.isFavourite == true;
await openFullScreenPlayer();
} else { final isDesktop = AdaptiveLayout.of(context).isDesktop;
await stopPlayer();
} final itemActions = [
return false; ItemActionButton(
}, label: Text(context.localized.audio),
direction: DismissDirection.vertical, icon: Consumer(
child: InkWell( builder: (context, ref, child) {
onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"), var volume = (player.lastState?.volume ?? 0) <= 0;
child: Card( return Icon(
elevation: 5, volume ? IconsaxPlusBold.volume_cross : IconsaxPlusBold.volume_high,
color: Theme.of(context).colorScheme.primaryContainer, );
child: SizedBox( },
height: floatingPlayerHeight, ),
child: LayoutBuilder(builder: (context, constraints) { action: () {
return Row( final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
children: [ player.setVolume(volume);
Flexible( }),
child: Padding( ItemActionButton(
padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: 0), label: Text(context.localized.stop),
child: Column( action: () async => stopPlayer(),
mainAxisSize: MainAxisSize.min, icon: const Icon(IconsaxPlusBold.stop),
children: [ ),
Expanded( ItemActionButton(
child: Padding( label: Text(isFavourite ? context.localized.removeAsFavorite : context.localized.addAsFavorite),
padding: const EdgeInsets.all(6), icon: Icon(
child: Row( color: isFavourite ? Colors.red : null,
spacing: 7, isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
children: [ ),
if (playbackInfo.state == VideoPlayerState.minimized) action: () async {
Card( final result = (await ref.read(userProvider.notifier).setAsFavorite(
child: AspectRatio( !isFavourite,
aspectRatio: 1.67, item?.id ?? "",
child: MouseRegion( ))
onEnter: (event) => setState(() => showExpandButton = true), ?.body;
onExit: (event) => setState(() => showExpandButton = false),
child: Stack( if (result != null) {
children: [ ref.read(playBackModel.notifier).update((state) => state?.updateUserData(result));
Hero( }
tag: videoPlayerHeroTag, },
child: player.videoWidget( ),
UniqueKey(), ];
BoxFit.fitHeight, return Padding(
) ?? padding:
const SizedBox.shrink(), MediaQuery.paddingOf(context).copyWith(top: 0, bottom: isDesktop ? 0 : MediaQuery.paddingOf(context).bottom),
), child: Dismissible(
Positioned.fill( key: const Key("CurrentlyPlayingBar"),
child: Tooltip( confirmDismiss: (direction) async {
message: "Expand player", if (direction == DismissDirection.up) {
waitDuration: const Duration(milliseconds: 500), await openFullScreenPlayer();
child: AnimatedOpacity( } else {
opacity: showExpandButton ? 1 : 0, await stopPlayer();
duration: const Duration(milliseconds: 125), }
child: Container( return false;
color: Colors.black.withValues(alpha: 0.6), },
child: FlatButton( direction: DismissDirection.vertical,
onTap: () async => openFullScreenPlayer(), child: InkWell(
child: const Icon(Icons.keyboard_arrow_up_rounded), onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"),
), child: Card(
), elevation: 5,
), color: Theme.of(context).colorScheme.primaryContainer,
), child: SizedBox(
) height: floatingPlayerHeight(context),
], child: LayoutBuilder(builder: (context, constraints) {
), return Column(
), mainAxisSize: MainAxisSize.min,
), children: [
), Expanded(
Expanded( child: Padding(
child: Column( padding: const EdgeInsets.all(6),
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
mainAxisSize: MainAxisSize.min, spacing: 12,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (playbackInfo.state == VideoPlayerState.minimized)
Card(
child: AspectRatio(
aspectRatio: 1.67,
child: MouseRegion(
onEnter: (event) => setState(() => showExpandButton = true),
onExit: (event) => setState(() => showExpandButton = false),
child: Stack(
children: [ children: [
Flexible( Hero(
child: Text( tag: videoPlayerHeroTag,
playbackModel?.title ?? "", child: player.videoWidget(
style: Theme.of(context).textTheme.titleMedium, UniqueKey(),
), BoxFit.fitHeight,
) ??
const SizedBox.shrink(),
), ),
if (playbackModel?.detailedName(context)?.isNotEmpty == true) Positioned.fill(
Flexible( child: Tooltip(
child: Text( message: "Expand player",
playbackModel?.detailedName(context) ?? "", waitDuration: const Duration(milliseconds: 500),
overflow: TextOverflow.ellipsis, child: AnimatedOpacity(
style: Theme.of(context).textTheme.bodyMedium?.copyWith( opacity: showExpandButton ? 1 : 0,
color: duration: const Duration(milliseconds: 125),
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), child: Container(
), color: Colors.black.withValues(alpha: 0.6),
child: FlatButton(
onTap: () async => openFullScreenPlayer(),
child: const Icon(Icons.keyboard_arrow_up_rounded),
),
),
), ),
), ),
)
], ],
), ),
), ),
if (!progress.isNaN && constraints.maxWidth > 500) ),
Text( ),
"${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), Expanded(
Padding( child: InkWell(
padding: const EdgeInsets.symmetric(horizontal: 12), onTap: () => item?.navigateTo(context),
child: IconButton.filledTonal( child: Column(
onPressed: () => ref.read(videoPlayerProvider).playOrPause(), crossAxisAlignment: CrossAxisAlignment.start,
icon: playbackInfo.playing mainAxisSize: MainAxisSize.min,
? const Icon(Icons.pause_rounded) children: [
: const Icon(Icons.play_arrow_rounded), Flexible(
), child: Text(
), item?.title ?? "",
if (constraints.maxWidth > 500) ...[ style: Theme.of(context).textTheme.titleMedium,
IconButton( maxLines: 1,
onPressed: () {
final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
player.setVolume(volume);
},
icon: Icon(
ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0
? IconsaxPlusBold.volume_cross
: IconsaxPlusBold.volume_high,
), ),
), ),
Tooltip( if (item?.detailedName(context)?.isNotEmpty == true)
message: context.localized.stop, Flexible(
waitDuration: const Duration(milliseconds: 500), child: Text(
child: IconButton( item?.detailedName(context) ?? "",
onPressed: () async => stopPlayer(), overflow: TextOverflow.ellipsis,
icon: const Icon(IconsaxPlusBold.stop), style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65),
),
maxLines: 1,
),
), ),
),
], ],
], ),
), ),
), ),
), Expanded(
LinearProgressIndicator( child: Row(
minHeight: 6, mainAxisAlignment: MainAxisAlignment.end,
backgroundColor: Colors.black.withValues(alpha: 0.25), children: [
color: Theme.of(context).colorScheme.primary, if (constraints.maxWidth > 500)
value: progress.clamp(0, 1), Flexible(
), child: Text(
], "${lastPosition.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"),
),
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: IconButton.filledTonal(
onPressed: () => ref.read(videoPlayerProvider).playOrPause(),
icon: playbackInfo.playing
? const Icon(Icons.pause_rounded)
: const Icon(Icons.play_arrow_rounded),
),
),
),
Flexible(
child: OverflowView.flexible(
builder: (context, remainingItemCount) => PopupMenuButton(
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
padding: EdgeInsets.zero,
itemBuilder: (context) => itemActions
.sublist(itemActions.length - remainingItemCount)
.map(
(e) => e.toPopupMenuItem(useIcons: true),
)
.toList(),
),
children: itemActions.map((e) => e.toButton()).toList(),
),
)
],
),
)
],
),
), ),
), ),
), AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer
], ? SizedBox(
); height: 8,
}), child: FladderSlider(
value: lastPosition.inMilliseconds.toDouble(),
min: 0.0,
max: playbackInfo.duration.inMilliseconds.toDouble(),
thumbWidth: 8,
onChangeStart: (value) {
setState(() {
changingSliderValue = true;
});
},
onChangeEnd: (value) async {
await player.seek(Duration(milliseconds: value ~/ 1));
await Future.delayed(const Duration(milliseconds: 250));
if (player.lastState?.playing == true) {
player.play();
}
setState(() {
lastPosition = Duration(milliseconds: value.toInt());
changingSliderValue = false;
});
},
onChanged: (value) {
setState(() {
lastPosition = Duration(milliseconds: value.toInt());
});
},
),
)
: LinearProgressIndicator(
minHeight: 8,
backgroundColor: Colors.black.withValues(alpha: 0.25),
color: Theme.of(context).colorScheme.primary,
value: (playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds)
.clamp(0, 1),
)
],
);
}),
),
), ),
), ),
), ),

View file

@ -74,13 +74,11 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
child: MouseRegion( child: MouseRegion(
child: Column( child: Column(
children: [ children: [
SizedBox(height: padding.top),
Expanded( Expanded(
child: Padding( child: Padding(
key: const Key('navigation_rail'), key: const Key('navigation_rail'),
padding: padding.copyWith(right: 0, top: isDesktop ? 8 : null), padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null),
child: Column( child: Column(
spacing: 2,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
@ -106,7 +104,6 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
], ],
), ),
), ),
const SizedBox(height: 8),
if (largeBar) ...[ if (largeBar) ...[
AnimatedFadeSize( AnimatedFadeSize(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
@ -290,7 +287,6 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
), ),
), ),
), ),
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16),
], ],
), ),
), ),

View file

@ -10,7 +10,6 @@ import 'package:fladder/routes/auto_router.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/theme_extensions.dart';
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
@ -58,9 +57,15 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
final isDesktop = AdaptiveLayout.of(context).isDesktop; final isDesktop = AdaptiveLayout.of(context).isDesktop;
final bottomPadding = isDesktop || kIsWeb ? 0.0 : MediaQuery.paddingOf(context).bottom; final mediaQuery = MediaQuery.of(context);
final paddingOf = mediaQuery.padding;
final viewPaddingOf = mediaQuery.viewPadding;
final bottomPadding = isDesktop || kIsWeb ? 0.0 : paddingOf.bottom;
final bottomViewPadding = isDesktop || kIsWeb ? 0.0 : viewPaddingOf.bottom;
final isHomeScreen = currentIndex != -1; final isHomeScreen = currentIndex != -1;
return PopScope( return PopScope(
canPop: currentIndex == 0, canPop: currentIndex == 0,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
@ -72,58 +77,66 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
Positioned.fill( Positioned.fill(
child: Padding( child: MediaQuery(
padding: EdgeInsets.only(bottom: showPlayerBar ? floatingPlayerHeight - 12 + bottomPadding : 0), data: mediaQuery.copyWith(
child: Scaffold( padding: paddingOf.copyWith(
key: _key, bottom: showPlayerBar ? floatingPlayerHeight(context) + 12 + bottomPadding : bottomPadding),
appBar: const FladderAppBar(), viewPadding: viewPaddingOf.copyWith(
extendBodyBehindAppBar: true, bottom: showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding),
resizeToAvoidBottomInset: false, ),
extendBody: true, //Builder to correctly apply new padding
floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen child: Builder(builder: (context) {
? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal return Scaffold(
: null, key: _key,
drawer: homeRoutes.any((element) => element.name.contains(currentLocation)) appBar: const FladderAppBar(),
? NestedNavigationDrawer( extendBodyBehindAppBar: true,
actionButton: null, resizeToAvoidBottomInset: false,
toggleExpanded: (value) => _key.currentState?.closeDrawer(), extendBody: true,
views: views, floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen
destinations: widget.destinations, ? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal
currentLocation: currentLocation, : null,
) drawer: homeRoutes.any((element) => element.name.contains(currentLocation))
: null, ? NestedNavigationDrawer(
bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone actionButton: null,
? HideOnScroll( toggleExpanded: (value) => _key.currentState?.closeDrawer(),
controller: AdaptiveLayout.scrollOf(context), views: views,
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), destinations: widget.destinations,
child: NestedBottomAppBar( currentLocation: currentLocation,
child: SizedBox( )
height: 65, : null,
child: Row( bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
mainAxisAlignment: MainAxisAlignment.spaceAround, ? HideOnScroll(
crossAxisAlignment: CrossAxisAlignment.stretch, controller: AdaptiveLayout.scrollOf(context),
children: widget.destinations forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
.map( child: NestedBottomAppBar(
(destination) => destination.toNavigationButton( child: SizedBox(
widget.currentRouteName == destination.route?.routeName, false, false), height: 65,
) child: Row(
.toList(), mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.destinations
.map(
(destination) => destination.toNavigationButton(
widget.currentRouteName == destination.route?.routeName, false, false),
)
.toList(),
),
), ),
), ),
), )
) : null,
: null, body: widget.nestedChild != null
body: widget.nestedChild != null ? NavigationBody(
? NavigationBody( child: widget.nestedChild!,
child: widget.nestedChild!, parentContext: context,
parentContext: context, currentIndex: currentIndex,
currentIndex: currentIndex, destinations: widget.destinations,
destinations: widget.destinations, currentLocation: currentLocation,
currentLocation: currentLocation, drawerKey: _key,
drawerKey: _key, )
) : null,
: null, );
), }),
), ),
), ),
Material( Material(
@ -131,19 +144,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
child: AnimatedFadeSize( child: AnimatedFadeSize(
child: Container( child: Container(
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( child: showPlayerBar ? const FloatingPlayerBar() : const SizedBox.shrink(),
color: context.colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: showPlayerBar
? Padding(
padding: EdgeInsets.only(bottom: bottomPadding),
child: const FloatingPlayerBar(),
)
: const SizedBox.shrink(),
), ),
), ),
) )

View file

@ -10,6 +10,7 @@ abstract class ItemAction {
PopupMenuEntry toPopupMenuItem({bool useIcons = false}); PopupMenuEntry toPopupMenuItem({bool useIcons = false});
Widget toLabel(); Widget toLabel();
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}); Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true});
Widget toButton();
} }
class ItemActionDivider extends ItemAction { class ItemActionDivider extends ItemAction {
@ -26,6 +27,9 @@ class ItemActionDivider extends ItemAction {
@override @override
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}) => const Divider(); Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}) => const Divider();
@override
Widget toButton() => Container();
} }
class ItemActionButton extends ItemAction { class ItemActionButton extends ItemAction {
@ -51,9 +55,10 @@ class ItemActionButton extends ItemAction {
} }
@override @override
MenuItemButton toMenuItemButton() { MenuItemButton toMenuItemButton() => MenuItemButton(leadingIcon: icon, onPressed: action, child: label);
return MenuItemButton(leadingIcon: icon, onPressed: action, child: label);
} @override
Widget toButton() => IconButton(onPressed: action, icon: icon ?? const SizedBox.shrink());
@override @override
PopupMenuItem toPopupMenuItem({bool useIcons = false}) { PopupMenuItem toPopupMenuItem({bool useIcons = false}) {