mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feat: UI 2.0 and other Improvements (#357)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
9ca06eaa37
commit
e7b5bb40ff
169 changed files with 4584 additions and 3626 deletions
|
|
@ -29,19 +29,15 @@ class AdaptiveFab {
|
|||
duration: const Duration(milliseconds: 250),
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: ElevatedButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: FilledButton.tonal(
|
||||
onPressed: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
child,
|
||||
const Spacer(),
|
||||
Flexible(child: Text(title)),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
spacing: 24,
|
||||
children: [
|
||||
child,
|
||||
Flexible(child: Text(title)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
|
||||
class BackgroundImage extends ConsumerStatefulWidget {
|
||||
final List<ItemBaseModel> items;
|
||||
final List<ImagesData> images;
|
||||
const BackgroundImage({this.items = const [], this.images = const [], super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _BackgroundImageState();
|
||||
}
|
||||
|
||||
class _BackgroundImageState extends ConsumerState<BackgroundImage> {
|
||||
ImageData? backgroundImage;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant BackgroundImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.items.length != widget.items.length || oldWidget.images.length != widget.images.length) {
|
||||
updateItems();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateItems();
|
||||
}
|
||||
|
||||
void updateItems() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((value) async {
|
||||
if (widget.images.isNotEmpty) {
|
||||
setState(() {
|
||||
backgroundImage = widget.images.shuffled().firstOrNull?.primary;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final randomItem = widget.items.shuffled().firstOrNull;
|
||||
if (widget.items.isEmpty) return;
|
||||
final itemId = switch (randomItem?.type) {
|
||||
FladderItemType.folder => randomItem?.id,
|
||||
FladderItemType.series => randomItem?.parentId ?? randomItem?.id,
|
||||
_ => randomItem?.id,
|
||||
} ??
|
||||
randomItem?.id;
|
||||
if (itemId == null) return;
|
||||
final apiProvider = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(
|
||||
itemId: itemId,
|
||||
);
|
||||
setState(() {
|
||||
backgroundImage = apiProvider.body?.parentBaseModel.getPosters?.randomBackDrop ??
|
||||
apiProvider.body?.getPosters?.randomBackDrop;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FladderImage(
|
||||
image: backgroundImage,
|
||||
fit: BoxFit.cover,
|
||||
blurOnly: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DestinationModel {
|
||||
final String label;
|
||||
|
|
@ -79,12 +81,13 @@ class DestinationModel {
|
|||
);
|
||||
}
|
||||
|
||||
NavigationButton toNavigationButton(bool selected, bool expanded) {
|
||||
NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded) {
|
||||
return NavigationButton(
|
||||
label: label,
|
||||
selected: selected,
|
||||
onPressed: action,
|
||||
horizontal: expanded,
|
||||
horizontal: horizontal,
|
||||
expanded: expanded,
|
||||
selectedIcon: selectedIcon!,
|
||||
icon: icon!,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
import 'package:fladder/screens/shared/default_title_bar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
bool get _isDesktop {
|
||||
if (kIsWeb) return false;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
|
|
@ -11,14 +11,15 @@ import 'package:fladder/providers/video_player_provider.dart';
|
|||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/video_player/video_player.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
|
||||
const videoPlayerHeroTag = "HeroPlayer";
|
||||
|
||||
const floatingPlayerHeight = 70.0;
|
||||
|
||||
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||
const FloatingPlayerBar({super.key});
|
||||
|
||||
|
|
@ -71,29 +72,29 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
|||
},
|
||||
direction: DismissDirection.vertical,
|
||||
child: InkWell(
|
||||
onLongPress: () {
|
||||
fladderSnackbar(context, title: "Swipe up/down to open/close the player");
|
||||
},
|
||||
onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"),
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 50, maxHeight: 85),
|
||||
child: SizedBox(
|
||||
height: floatingPlayerHeight,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
if (playbackInfo.state == VideoPlayerState.minimized)
|
||||
Card(
|
||||
child: SizedBox(
|
||||
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(
|
||||
|
|
@ -131,72 +132,76 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
playbackModel?.title ?? "",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
if (playbackModel?.detailedName(context)?.isNotEmpty == true)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
playbackModel?.detailedName(context) ?? "",
|
||||
playbackModel?.title ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
Tooltip(
|
||||
message: context.localized.stop,
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: IconButton(
|
||||
onPressed: () async => stopPlayer(),
|
||||
icon: const Icon(IconsaxPlusBold.stop),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
].addInBetween(const SizedBox(width: 6)),
|
||||
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,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: context.localized.stop,
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: IconButton(
|
||||
onPressed: () async => stopPlayer(),
|
||||
icon: const Icon(IconsaxPlusBold.stop),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.25),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
value: progress.clamp(0, 1),
|
||||
),
|
||||
],
|
||||
LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.25),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
value: progress.clamp(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,21 +2,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_drawer.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/side_navigation_bar.dart';
|
||||
|
||||
class NavigationBody extends ConsumerStatefulWidget {
|
||||
final BuildContext parentContext;
|
||||
|
|
@ -40,7 +33,7 @@ class NavigationBody extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _NavigationBodyState extends ConsumerState<NavigationBody> {
|
||||
bool expandedSideBar = true;
|
||||
double currentSideBarWidth = 80;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -52,9 +45,9 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final views = ref.watch(viewsProvider.select((value) => value.views));
|
||||
final hasOverlay = AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ||
|
||||
homeRoutes.any((element) => element.name.contains(context.router.current.name));
|
||||
|
||||
ref.listen(
|
||||
clientSettingsProvider,
|
||||
(previous, next) {
|
||||
|
|
@ -66,56 +59,28 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
|
|||
},
|
||||
);
|
||||
|
||||
return switch (AdaptiveLayout.layoutOf(context)) {
|
||||
ViewSize.phone => MediaQuery.removePadding(
|
||||
context: widget.parentContext,
|
||||
Widget paddedChild() => MediaQuery(
|
||||
data: semiNestedPadding(widget.parentContext, hasOverlay),
|
||||
child: widget.child,
|
||||
),
|
||||
ViewSize.tablet => Row(
|
||||
children: [
|
||||
AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: hasOverlay ? navigationRail(context) : const SizedBox(),
|
||||
),
|
||||
Flexible(
|
||||
child: MediaQuery(
|
||||
data: semiNestedPadding(context, hasOverlay),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
|
||||
return switch (AdaptiveLayout.layoutOf(context)) {
|
||||
ViewSize.phone => paddedChild(),
|
||||
ViewSize.tablet => hasOverlay
|
||||
? SideNavigationBar(
|
||||
currentIndex: widget.currentIndex,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: widget.currentLocation,
|
||||
child: paddedChild(),
|
||||
scaffoldKey: widget.drawerKey,
|
||||
)
|
||||
],
|
||||
),
|
||||
ViewSize.desktop => Row(
|
||||
children: [
|
||||
AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 125),
|
||||
child: hasOverlay
|
||||
? expandedSideBar
|
||||
? MediaQuery.removePadding(
|
||||
context: widget.parentContext,
|
||||
child: NestedNavigationDrawer(
|
||||
isExpanded: expandedSideBar,
|
||||
actionButton: actionButton(),
|
||||
toggleExpanded: (value) {
|
||||
setState(() {
|
||||
expandedSideBar = value;
|
||||
});
|
||||
},
|
||||
views: views,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: widget.currentLocation,
|
||||
),
|
||||
)
|
||||
: navigationRail(context)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Flexible(
|
||||
child: MediaQuery(
|
||||
data: semiNestedPadding(context, hasOverlay),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
],
|
||||
: paddedChild(),
|
||||
ViewSize.desktop => SideNavigationBar(
|
||||
currentIndex: widget.currentIndex,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: widget.currentLocation,
|
||||
child: paddedChild(),
|
||||
scaffoldKey: widget.drawerKey,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
@ -126,89 +91,4 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
|
|||
padding: paddingOf.copyWith(left: hasOverlay ? 0 : paddingOf.left),
|
||||
);
|
||||
}
|
||||
|
||||
AdaptiveFab? actionButton() {
|
||||
return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length)
|
||||
? widget.destinations[widget.currentIndex].floatingActionButton
|
||||
: null;
|
||||
}
|
||||
|
||||
Widget navigationRail(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (AdaptiveLayout.of(context).isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Fladder",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
},
|
||||
if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS)
|
||||
SizedBox(height: MediaQuery.of(context).padding.top),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
key: const Key('navigation_rail'),
|
||||
padding:
|
||||
MediaQuery.paddingOf(context).copyWith(right: 0, top: AdaptiveLayout.of(context).isDesktop ? 8 : null),
|
||||
child: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (AdaptiveLayout.layoutOf(context) != ViewSize.desktop) {
|
||||
widget.drawerKey.currentState?.openDrawer();
|
||||
} else {
|
||||
setState(() {
|
||||
expandedSideBar = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconsaxPlusBold.menu),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual) ...[
|
||||
const SizedBox(height: 8),
|
||||
AnimatedFadeSize(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
child: actionButton()?.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
IconTheme(
|
||||
data: const IconThemeData(size: 28),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...widget.destinations.mapIndexed(
|
||||
(index, destination) => destination.toNavigationButton(widget.currentIndex == index, false),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: widget.currentLocation.contains(const SettingsRoute().routeName)
|
||||
? Card(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(IconsaxPlusBold.setting_3),
|
||||
),
|
||||
)
|
||||
: const SettingsUserIcon()),
|
||||
),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/util/widget_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
class NavigationButton extends ConsumerStatefulWidget {
|
||||
final String? label;
|
||||
final Widget selectedIcon;
|
||||
final Widget icon;
|
||||
final bool horizontal;
|
||||
final bool expanded;
|
||||
final Function()? onPressed;
|
||||
final Function()? onLongPress;
|
||||
final List<ItemAction> trailing;
|
||||
final bool selected;
|
||||
final Duration duration;
|
||||
const NavigationButton({
|
||||
|
|
@ -17,8 +21,11 @@ class NavigationButton extends ConsumerStatefulWidget {
|
|||
required this.selectedIcon,
|
||||
required this.icon,
|
||||
this.horizontal = false,
|
||||
this.expanded = false,
|
||||
this.onPressed,
|
||||
this.onLongPress,
|
||||
this.selected = false,
|
||||
this.trailing = const [],
|
||||
this.duration = const Duration(milliseconds: 125),
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -28,106 +35,119 @@ class NavigationButton extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _NavigationButtonState extends ConsumerState<NavigationButton> {
|
||||
bool showPopupButton = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.longPress,
|
||||
message: widget.label ?? "",
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
final foreGroundColor = widget.selected
|
||||
? widget.expanded
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: ElevatedButton(
|
||||
onHover: (value) => setState(() => showPopupButton = value),
|
||||
style: ButtonStyle(
|
||||
elevation: const WidgetStatePropertyAll(0),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
widget.expanded && widget.selected ? Theme.of(context).colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
iconSize: const WidgetStatePropertyAll(24),
|
||||
iconColor: WidgetStateProperty.resolveWith((states) {
|
||||
return foreGroundColor;
|
||||
}),
|
||||
foregroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return foreGroundColor;
|
||||
})),
|
||||
onPressed: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: widget.horizontal
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: getChildren(context),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: getChildren(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> getChildren(BuildContext context) {
|
||||
return [
|
||||
Flexible(
|
||||
child: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
elevation: const WidgetStatePropertyAll(0),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
backgroundColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
iconSize: const WidgetStatePropertyAll(24),
|
||||
iconColor: WidgetStateProperty.resolveWith((states) {
|
||||
return widget.selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45);
|
||||
}),
|
||||
foregroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return widget.selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45);
|
||||
})),
|
||||
onPressed: widget.onPressed,
|
||||
child: AnimatedContainer(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: widget.duration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: widget.selected ? 16 : 0,
|
||||
margin: const EdgeInsets.only(top: 1.5),
|
||||
width: 6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withValues(alpha: widget.selected && !widget.expanded ? 1 : 0),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: widget.duration,
|
||||
child: widget.selected
|
||||
? widget.selectedIcon.setKey(Key("${widget.label}+selected"))
|
||||
: widget.icon.setKey(Key("${widget.label}+normal")),
|
||||
child: widget.selected ? widget.selectedIcon : widget.icon,
|
||||
),
|
||||
if (widget.horizontal && widget.label != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: _Label(widget: widget),
|
||||
)
|
||||
const SizedBox(width: 6),
|
||||
if (widget.horizontal && widget.expanded) ...[
|
||||
if (widget.label != null)
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 80),
|
||||
child: Text(
|
||||
widget.label!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.trailing.isNotEmpty)
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 125),
|
||||
opacity: showPopupButton ? 1 : 0,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
iconColor: foreGroundColor,
|
||||
iconSize: 18,
|
||||
itemBuilder: (context) => widget.trailing.popupMenuItems(useIcons: true),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: EdgeInsets.only(top: widget.selected ? 8 : 0),
|
||||
height: widget.selected ? 6 : 0,
|
||||
width: widget.selected ? 14 : 0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: widget.selected ? 1 : 0),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: widget.duration,
|
||||
child: widget.selected ? widget.selectedIcon : widget.icon,
|
||||
),
|
||||
if (widget.label != null && widget.horizontal && widget.expanded)
|
||||
Flexible(child: Text(widget.label!))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: EdgeInsets.only(top: widget.selected ? 4 : 0),
|
||||
height: widget.selected ? 6 : 0,
|
||||
width: widget.selected ? 14 : 0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: widget.selected ? 1 : 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _Label extends StatelessWidget {
|
||||
const _Label({required this.widget});
|
||||
|
||||
final NavigationButton widget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
widget.label!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style:
|
||||
Theme.of(context).textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||
|
|
@ -54,7 +53,7 @@ class NestedNavigationDrawer extends ConsumerWidget {
|
|||
),
|
||||
IconButton(
|
||||
onPressed: () => toggleExpanded(false),
|
||||
icon: const Icon(IconsaxPlusLinear.menu_1),
|
||||
icon: const Icon(IconsaxPlusLinear.sidebar_left),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -71,16 +70,18 @@ class NestedNavigationDrawer extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
...destinations.map((destination) => DrawerListButton(
|
||||
label: destination.label,
|
||||
selected: context.router.current.name == destination.route?.routeName,
|
||||
selectedIcon: destination.selectedIcon!,
|
||||
icon: destination.icon!,
|
||||
onPressed: () {
|
||||
destination.action!();
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
)),
|
||||
...destinations.map(
|
||||
(destination) => DrawerListButton(
|
||||
label: destination.label,
|
||||
selected: context.router.current.name == destination.route?.routeName,
|
||||
selectedIcon: destination.selectedIcon!,
|
||||
icon: destination.icon!,
|
||||
onPressed: () {
|
||||
destination.action!();
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (views.isNotEmpty) ...{
|
||||
const Divider(indent: 28, endIndent: 28),
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
class SettingsUserIcon extends ConsumerWidget {
|
||||
|
|
@ -15,13 +15,11 @@ class SettingsUserIcon extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final users = ref.watch(userProvider);
|
||||
final user = ref.watch(userProvider);
|
||||
return Tooltip(
|
||||
message: context.localized.settings,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: UserIcon(
|
||||
user: users,
|
||||
cornerRadius: 200,
|
||||
child: FlatButton(
|
||||
onLongPress: () => context.router.push(const LockRoute()),
|
||||
onTap: () {
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
||||
|
|
@ -30,6 +28,10 @@ class SettingsUserIcon extends ConsumerWidget {
|
|||
context.router.push(const ClientSettingsRoute());
|
||||
}
|
||||
},
|
||||
child: UserIcon(
|
||||
user: user,
|
||||
cornerRadius: 200,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,258 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:overflow_view/overflow_view.dart';
|
||||
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
|
||||
class SideNavigationBar extends ConsumerStatefulWidget {
|
||||
final int currentIndex;
|
||||
final List<DestinationModel> destinations;
|
||||
final String currentLocation;
|
||||
final Widget child;
|
||||
final GlobalKey<ScaffoldState> scaffoldKey;
|
||||
const SideNavigationBar({
|
||||
required this.currentIndex,
|
||||
required this.destinations,
|
||||
required this.currentLocation,
|
||||
required this.child,
|
||||
required this.scaffoldKey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _SideNavigationBarState();
|
||||
}
|
||||
|
||||
class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||
bool expandedSideBar = false;
|
||||
bool showOnHover = false;
|
||||
Timer? timer;
|
||||
double currentWidth = 80;
|
||||
|
||||
void startTimer() {
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(milliseconds: 650), () {
|
||||
setState(() {
|
||||
showOnHover = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void stopTimer() {
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(milliseconds: 350), () {
|
||||
setState(() {
|
||||
showOnHover = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final views = ref.watch(viewsProvider.select((value) => value.views));
|
||||
final expandedWidth = 250.0;
|
||||
final padding = MediaQuery.paddingOf(context);
|
||||
|
||||
final collapsedWidth = 90.0 + padding.left;
|
||||
final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||
final fullyExpanded = largeBar ? expandedSideBar : false;
|
||||
final shouldExpand = showOnHover || fullyExpanded;
|
||||
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||
return Stack(
|
||||
children: [
|
||||
AdaptiveLayoutBuilder(
|
||||
adaptiveLayout: AdaptiveLayout.of(context).copyWith(
|
||||
sideBarWidth: fullyExpanded ? expandedWidth : collapsedWidth,
|
||||
),
|
||||
child: (context) => widget.child,
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
alignment: Alignment.topLeft,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
|
||||
width: shouldExpand ? expandedWidth : collapsedWidth,
|
||||
child: MouseRegion(
|
||||
onEnter: (value) => startTimer(),
|
||||
onExit: (event) => stopTimer(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Fladder",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
},
|
||||
if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) SizedBox(height: padding.top),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
key: const Key('navigation_rail'),
|
||||
padding: padding.copyWith(right: 0, top: isDesktop ? 8 : null),
|
||||
child: Column(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Align(
|
||||
alignment: largeBar && expandedSideBar ? Alignment.centerRight : Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
|
||||
child: IconButton(
|
||||
onPressed: !largeBar
|
||||
? () => widget.scaffoldKey.currentState?.openDrawer()
|
||||
: () => setState(() {
|
||||
expandedSideBar = !expandedSideBar;
|
||||
if (!expandedSideBar) {
|
||||
showOnHover = false;
|
||||
}
|
||||
}),
|
||||
icon: Icon(
|
||||
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (largeBar) ...[
|
||||
AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: shouldExpand ? actionButton(context).extended : actionButton(context).normal,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 2,
|
||||
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
...widget.destinations.mapIndexed(
|
||||
(index, destination) =>
|
||||
destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand),
|
||||
),
|
||||
if (views.isNotEmpty && largeBar) ...[
|
||||
const Divider(
|
||||
indent: 32,
|
||||
endIndent: 32,
|
||||
),
|
||||
Flexible(
|
||||
child: OverflowView.flexible(
|
||||
direction: Axis.vertical,
|
||||
spacing: 4,
|
||||
children: views.map(
|
||||
(view) {
|
||||
final actions = [
|
||||
ItemActionButton(
|
||||
label: Text(context.localized.scanLibrary),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
action: () => showRefreshPopup(context, view.id, view.name),
|
||||
)
|
||||
];
|
||||
return view.toNavigationButton(
|
||||
context.router.currentUrl.contains(view.id),
|
||||
true,
|
||||
shouldExpand,
|
||||
() => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
|
||||
onLongPress: () => showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: actions.listTileItems(context, useIcons: true),
|
||||
),
|
||||
),
|
||||
trailing: actions,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
builder: (context, remaining) {
|
||||
return PopupMenuButton(
|
||||
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: NavigationButton(
|
||||
label: context.localized.other,
|
||||
selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down),
|
||||
icon: const Icon(IconsaxPlusLinear.arrow_square_down),
|
||||
expanded: shouldExpand,
|
||||
horizontal: true,
|
||||
),
|
||||
itemBuilder: (context) => views
|
||||
.sublist(views.length - remaining)
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(e.collectionType.iconOutlined),
|
||||
Text(e.name),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
NavigationButton(
|
||||
label: context.localized.settings,
|
||||
selected: widget.currentLocation.contains(const SettingsRoute().routeName),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.setting_3),
|
||||
horizontal: true,
|
||||
expanded: shouldExpand,
|
||||
icon: const SizedBox(height: 32, child: SettingsUserIcon()),
|
||||
onPressed: () {
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
||||
context.router.push(const SettingsRoute());
|
||||
} else {
|
||||
context.router.push(const ClientSettingsRoute());
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
AdaptiveFab actionButton(BuildContext context) {
|
||||
return ((widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length)
|
||||
? widget.destinations[widget.currentIndex].floatingActionButton
|
||||
: null) ??
|
||||
AdaptiveFab(
|
||||
context: context,
|
||||
title: context.localized.search,
|
||||
key: const Key("Search"),
|
||||
onPressed: () => context.router.navigate(LibrarySearchRoute()),
|
||||
child: const Icon(IconsaxPlusLinear.search_normal_1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
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.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';
|
||||
|
|
@ -51,11 +52,15 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
|
||||
final views = ref.watch(viewsProvider.select((value) => value.views));
|
||||
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
|
||||
final showPlayerBar = playerState == VideoPlayerState.minimized;
|
||||
|
||||
final isHomeRoutes = homeRoutes.any((element) => element.name.contains(context.router.current.name));
|
||||
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||
|
||||
final bottomPadding = isDesktop || kIsWeb ? 0.0 : MediaQuery.paddingOf(context).bottom;
|
||||
|
||||
final isHomeScreen = currentIndex != -1;
|
||||
return PopScope(
|
||||
canPop: currentIndex == 0,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
|
|
@ -63,67 +68,86 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
|||
widget.destinations.first.action!();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
key: _key,
|
||||
appBar: const FladderAppBar(),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
floatingActionButtonAnimator:
|
||||
playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null,
|
||||
floatingActionButtonLocation:
|
||||
playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null,
|
||||
floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeRoutes
|
||||
? switch (playerState) {
|
||||
VideoPlayerState.minimized => AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: FloatingPlayerBar(),
|
||||
)
|
||||
: null,
|
||||
_ => currentIndex != -1
|
||||
child: Stack(
|
||||
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,
|
||||
}
|
||||
: null,
|
||||
drawer: NestedNavigationDrawer(
|
||||
actionButton: null,
|
||||
toggleExpanded: (value) => _key.currentState?.closeDrawer(),
|
||||
views: views,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
),
|
||||
bottomNavigationBar: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
|
||||
? HideOnScroll(
|
||||
controller: AdaptiveLayout.scrollOf(context),
|
||||
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
|
||||
child: NestedBottomAppBar(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: widget.destinations
|
||||
.map(
|
||||
(destination) => destination.toNavigationButton(
|
||||
widget.currentRouteName == destination.route?.routeName, false),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: widget.nestedChild != null
|
||||
? NavigationBody(
|
||||
child: widget.nestedChild!,
|
||||
parentContext: context,
|
||||
currentIndex: currentIndex,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
drawerKey: _key,
|
||||
)
|
||||
: null,
|
||||
child: showPlayerBar
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(bottom: bottomPadding),
|
||||
child: const FloatingPlayerBar(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue