mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Improved floating player bar
This commit is contained in:
parent
82e09b3e0c
commit
013722fc96
8 changed files with 338 additions and 210 deletions
|
|
@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_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_streams_model.dart';
|
||||
import 'package:fladder/models/items/trick_play_model.dart';
|
||||
|
|
@ -115,6 +116,15 @@ class DirectPlaybackModel extends PlaybackModel {
|
|||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
DirectPlaybackModel? updateUserData(UserData userData) {
|
||||
return copyWith(
|
||||
item: item.copyWith(
|
||||
userData: userData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/models/item_base_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_streams_model.dart';
|
||||
import 'package:fladder/models/items/trick_play_model.dart';
|
||||
|
|
@ -96,6 +97,15 @@ class OfflinePlaybackModel extends PlaybackModel {
|
|||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
OfflinePlaybackModel? updateUserData(UserData userData) {
|
||||
return copyWith(
|
||||
item: item.copyWith(
|
||||
userData: userData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)';
|
||||
|
||||
|
|
|
|||
|
|
@ -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/items/chapters_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_streams_model.dart';
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
|
|
@ -84,6 +85,8 @@ class PlaybackModel {
|
|||
|
||||
Future<Duration>? startDuration() async => item.userData.playBackPosition;
|
||||
|
||||
PlaybackModel? updateUserData(UserData userData) => throw UnimplementedError();
|
||||
|
||||
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
|
||||
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
|
||||
Future<PlaybackModel>? setQualityOption(Map<Bitrate, bool> map) => throw UnimplementedError();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_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_streams_model.dart';
|
||||
import 'package:fladder/models/items/trick_play_model.dart';
|
||||
|
|
@ -51,7 +52,7 @@ class TranscodePlaybackModel extends PlaybackModel {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<TranscodePlaybackModel>? setQualityOption(Map<Bitrate, bool> map) async => copyWith(bitRateOptions: map);
|
||||
Future<TranscodePlaybackModel>? setQualityOption(Map<Bitrate, bool> map) async => copyWith(bitRateOptions: map);
|
||||
|
||||
@override
|
||||
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
|
||||
|
|
@ -114,6 +115,15 @@ class TranscodePlaybackModel extends PlaybackModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
TranscodePlaybackModel? updateUserData(UserData userData) {
|
||||
return copyWith(
|
||||
item: item.copyWith(
|
||||
userData: userData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:overflow_view/overflow_view.dart';
|
||||
import 'package:window_manager/window_manager.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/screens/shared/fladder_snackbar.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/localization_helper.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 floatingPlayerHeight = 70.0;
|
||||
double floatingPlayerHeight(BuildContext context) => switch (AdaptiveLayout.viewSizeOf(context)) {
|
||||
ViewSize.phone => 75,
|
||||
ViewSize.tablet => 85,
|
||||
ViewSize.desktop => 95,
|
||||
};
|
||||
|
||||
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||
const FloatingPlayerBar({super.key});
|
||||
|
|
@ -29,6 +36,8 @@ class FloatingPlayerBar extends ConsumerStatefulWidget {
|
|||
|
||||
class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||
bool showExpandButton = false;
|
||||
bool changingSliderValue = false;
|
||||
Duration lastPosition = Duration.zero;
|
||||
|
||||
Future<void> openFullScreenPlayer() async {
|
||||
setState(() => showExpandButton = false);
|
||||
|
|
@ -58,155 +67,239 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
|||
Widget build(BuildContext context) {
|
||||
final playbackInfo = ref.watch(mediaPlaybackProvider);
|
||||
final player = ref.watch(videoPlayerProvider);
|
||||
final playbackModel = ref.watch(playBackModel.select((value) => value?.item));
|
||||
final progress = playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds;
|
||||
return Dismissible(
|
||||
key: const Key("CurrentlyPlayingBar"),
|
||||
confirmDismiss: (direction) async {
|
||||
if (direction == DismissDirection.up) {
|
||||
await openFullScreenPlayer();
|
||||
} else {
|
||||
await stopPlayer();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
direction: DismissDirection.vertical,
|
||||
child: InkWell(
|
||||
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,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
spacing: 7,
|
||||
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: [
|
||||
Hero(
|
||||
tag: videoPlayerHeroTag,
|
||||
child: player.videoWidget(
|
||||
UniqueKey(),
|
||||
BoxFit.fitHeight,
|
||||
) ??
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Tooltip(
|
||||
message: "Expand player",
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: AnimatedOpacity(
|
||||
opacity: showExpandButton ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
child: FlatButton(
|
||||
onTap: () async => openFullScreenPlayer(),
|
||||
child: const Icon(Icons.keyboard_arrow_up_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
final item = ref.watch(playBackModel.select((value) => value?.item));
|
||||
if (!changingSliderValue) {
|
||||
lastPosition = playbackInfo.position;
|
||||
}
|
||||
|
||||
var isFavourite = item?.userData.isFavourite == true;
|
||||
|
||||
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||
|
||||
final itemActions = [
|
||||
ItemActionButton(
|
||||
label: Text(context.localized.audio),
|
||||
icon: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
var volume = (player.lastState?.volume ?? 0) <= 0;
|
||||
return Icon(
|
||||
volume ? IconsaxPlusBold.volume_cross : IconsaxPlusBold.volume_high,
|
||||
);
|
||||
},
|
||||
),
|
||||
action: () {
|
||||
final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
|
||||
player.setVolume(volume);
|
||||
}),
|
||||
ItemActionButton(
|
||||
label: Text(context.localized.stop),
|
||||
action: () async => stopPlayer(),
|
||||
icon: const Icon(IconsaxPlusBold.stop),
|
||||
),
|
||||
ItemActionButton(
|
||||
label: Text(isFavourite ? context.localized.removeAsFavorite : context.localized.addAsFavorite),
|
||||
icon: Icon(
|
||||
color: isFavourite ? Colors.red : null,
|
||||
isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
|
||||
),
|
||||
action: () async {
|
||||
final result = (await ref.read(userProvider.notifier).setAsFavorite(
|
||||
!isFavourite,
|
||||
item?.id ?? "",
|
||||
))
|
||||
?.body;
|
||||
|
||||
if (result != null) {
|
||||
ref.read(playBackModel.notifier).update((state) => state?.updateUserData(result));
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
return Padding(
|
||||
padding:
|
||||
MediaQuery.paddingOf(context).copyWith(top: 0, bottom: isDesktop ? 0 : MediaQuery.paddingOf(context).bottom),
|
||||
child: Dismissible(
|
||||
key: const Key("CurrentlyPlayingBar"),
|
||||
confirmDismiss: (direction) async {
|
||||
if (direction == DismissDirection.up) {
|
||||
await openFullScreenPlayer();
|
||||
} else {
|
||||
await stopPlayer();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
direction: DismissDirection.vertical,
|
||||
child: InkWell(
|
||||
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(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
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: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
playbackModel?.title ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Hero(
|
||||
tag: videoPlayerHeroTag,
|
||||
child: player.videoWidget(
|
||||
UniqueKey(),
|
||||
BoxFit.fitHeight,
|
||||
) ??
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
if (playbackModel?.detailedName(context)?.isNotEmpty == true)
|
||||
Flexible(
|
||||
child: Text(
|
||||
playbackModel?.detailedName(context) ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Tooltip(
|
||||
message: "Expand player",
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: AnimatedOpacity(
|
||||
opacity: showExpandButton ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
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}"),
|
||||
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),
|
||||
),
|
||||
),
|
||||
if (constraints.maxWidth > 500) ...[
|
||||
IconButton(
|
||||
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,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => item?.navigateTo(context),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item?.title ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: context.localized.stop,
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: IconButton(
|
||||
onPressed: () async => stopPlayer(),
|
||||
icon: const Icon(IconsaxPlusBold.stop),
|
||||
if (item?.detailedName(context)?.isNotEmpty == true)
|
||||
Flexible(
|
||||
child: Text(
|
||||
item?.detailedName(context) ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.25),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
value: progress.clamp(0, 1),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (constraints.maxWidth > 500)
|
||||
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),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -74,13 +74,11 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
|||
child: MouseRegion(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: padding.top),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
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(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
|
|
@ -106,7 +104,6 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
|||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (largeBar) ...[
|
||||
AnimatedFadeSize(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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/nested_bottom_appbar.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/fladder_app_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 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;
|
||||
|
||||
return PopScope(
|
||||
canPop: currentIndex == 0,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
|
|
@ -72,58 +77,66 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
|||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: showPlayerBar ? floatingPlayerHeight - 12 + bottomPadding : 0),
|
||||
child: Scaffold(
|
||||
key: _key,
|
||||
appBar: const FladderAppBar(),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen
|
||||
? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal
|
||||
: null,
|
||||
drawer: homeRoutes.any((element) => element.name.contains(currentLocation))
|
||||
? NestedNavigationDrawer(
|
||||
actionButton: null,
|
||||
toggleExpanded: (value) => _key.currentState?.closeDrawer(),
|
||||
views: views,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
|
||||
? HideOnScroll(
|
||||
controller: AdaptiveLayout.scrollOf(context),
|
||||
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
|
||||
child: NestedBottomAppBar(
|
||||
child: SizedBox(
|
||||
height: 65,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: widget.destinations
|
||||
.map(
|
||||
(destination) => destination.toNavigationButton(
|
||||
widget.currentRouteName == destination.route?.routeName, false, false),
|
||||
)
|
||||
.toList(),
|
||||
child: MediaQuery(
|
||||
data: mediaQuery.copyWith(
|
||||
padding: paddingOf.copyWith(
|
||||
bottom: showPlayerBar ? floatingPlayerHeight(context) + 12 + bottomPadding : bottomPadding),
|
||||
viewPadding: viewPaddingOf.copyWith(
|
||||
bottom: showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding),
|
||||
),
|
||||
//Builder to correctly apply new padding
|
||||
child: Builder(builder: (context) {
|
||||
return Scaffold(
|
||||
key: _key,
|
||||
appBar: const FladderAppBar(),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen
|
||||
? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal
|
||||
: null,
|
||||
drawer: homeRoutes.any((element) => element.name.contains(currentLocation))
|
||||
? NestedNavigationDrawer(
|
||||
actionButton: null,
|
||||
toggleExpanded: (value) => _key.currentState?.closeDrawer(),
|
||||
views: views,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
|
||||
? HideOnScroll(
|
||||
controller: AdaptiveLayout.scrollOf(context),
|
||||
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
|
||||
child: NestedBottomAppBar(
|
||||
child: SizedBox(
|
||||
height: 65,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: widget.destinations
|
||||
.map(
|
||||
(destination) => destination.toNavigationButton(
|
||||
widget.currentRouteName == destination.route?.routeName, false, false),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: widget.nestedChild != null
|
||||
? NavigationBody(
|
||||
child: widget.nestedChild!,
|
||||
parentContext: context,
|
||||
currentIndex: currentIndex,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
drawerKey: _key,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: widget.nestedChild != null
|
||||
? NavigationBody(
|
||||
child: widget.nestedChild!,
|
||||
parentContext: context,
|
||||
currentIndex: currentIndex,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
drawerKey: _key,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
|
|
@ -131,19 +144,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
|||
child: AnimatedFadeSize(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
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(),
|
||||
child: showPlayerBar ? const FloatingPlayerBar() : const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ abstract class ItemAction {
|
|||
PopupMenuEntry toPopupMenuItem({bool useIcons = false});
|
||||
Widget toLabel();
|
||||
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true});
|
||||
Widget toButton();
|
||||
}
|
||||
|
||||
class ItemActionDivider extends ItemAction {
|
||||
|
|
@ -26,6 +27,9 @@ class ItemActionDivider extends ItemAction {
|
|||
|
||||
@override
|
||||
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}) => const Divider();
|
||||
|
||||
@override
|
||||
Widget toButton() => Container();
|
||||
}
|
||||
|
||||
class ItemActionButton extends ItemAction {
|
||||
|
|
@ -51,9 +55,10 @@ class ItemActionButton extends ItemAction {
|
|||
}
|
||||
|
||||
@override
|
||||
MenuItemButton toMenuItemButton() {
|
||||
return MenuItemButton(leadingIcon: icon, onPressed: action, child: label);
|
||||
}
|
||||
MenuItemButton toMenuItemButton() => MenuItemButton(leadingIcon: icon, onPressed: action, child: label);
|
||||
|
||||
@override
|
||||
Widget toButton() => IconButton(onPressed: action, icon: icon ?? const SizedBox.shrink());
|
||||
|
||||
@override
|
||||
PopupMenuItem toPopupMenuItem({bool useIcons = false}) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue