chore: Lots of bug fixes and navigation improvements

This commit is contained in:
PartyDonut 2025-09-01 20:21:36 +02:00
parent 9bb5e81812
commit 92d5391b93
35 changed files with 513 additions and 455 deletions

View file

@ -7,6 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:overflow_view/overflow_view.dart';
import 'package:fladder/models/collection_types.dart';
import 'package:fladder/models/library_filter_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
@ -24,6 +25,10 @@ import 'package:fladder/widgets/shared/custom_tooltip.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
const _fullScreenRoutes = {
PhotoViewerRoute.name,
};
class SideNavigationBar extends ConsumerStatefulWidget {
final int currentIndex;
final List<DestinationModel> destinations;
@ -60,6 +65,8 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
final shouldExpand = fullyExpanded;
final isDesktop = AdaptiveLayout.of(context).isDesktop;
final fullScreenChildRoute = _fullScreenRoutes.contains(context.router.current.name);
return Stack(
children: [
AdaptiveLayoutBuilder(
@ -69,231 +76,244 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
),
child: (context) => widget.child,
),
Container(
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
width: shouldExpand ? expandedWidth : collapsedWidth,
child: MouseRegion(
child: Padding(
key: const Key('navigation_rail'),
padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null),
child: Column(
spacing: 2,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (expandedSideBar) ...[
Expanded(child: Text(context.localized.navigation)),
],
Opacity(
opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
child: IconButton(
onPressed: !largeBar
? () => widget.scaffoldKey.currentState?.openDrawer()
: () => setState(() => expandedSideBar = !expandedSideBar),
icon: Icon(
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
),
),
)
],
),
),
if (largeBar) ...[
AnimatedFadeSize(
duration: const Duration(milliseconds: 250),
child: shouldExpand ? actionButton(context).extended : actionButton(context).normal,
),
],
Expanded(
child: Column(
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
...widget.destinations.mapIndexed(
(index, destination) => CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
destination.label,
style: Theme.of(context).textTheme.titleSmall,
),
),
),
position: TooltipPosition.right,
child: destination.toNavigationButton(
widget.currentIndex == index,
true,
shouldExpand,
),
),
IgnorePointer(
ignoring: fullScreenChildRoute,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: !fullScreenChildRoute ? 1 : 0,
child: Container(
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
width: shouldExpand ? expandedWidth : collapsedWidth,
child: MouseRegion(
child: Padding(
key: const Key('navigation_rail'),
padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null),
child: Column(
spacing: 2,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (expandedSideBar) ...[
Expanded(child: Text(context.localized.navigation)),
],
Opacity(
opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
child: IconButton(
onPressed: !largeBar
? () => widget.scaffoldKey.currentState?.openDrawer()
: () => setState(() => expandedSideBar = !expandedSideBar),
icon: Icon(
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
),
),
)
],
),
if (views.isNotEmpty && largeBar) ...[
const Divider(
indent: 32,
endIndent: 32,
),
Flexible(
child: OverflowView.flexible(
direction: Axis.vertical,
spacing: 4,
children: views.map(
(view) {
final selected = context.router.currentUrl.contains(view.id);
final actions = [
ItemActionButton(
label: Text(context.localized.scanLibrary),
icon: const Icon(IconsaxPlusLinear.refresh),
action: () => showRefreshPopup(context, view.id, view.name),
)
];
return CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
view.name,
style: Theme.of(context).textTheme.titleSmall,
),
),
),
if (largeBar) ...[
AnimatedFadeSize(
duration: const Duration(milliseconds: 250),
child: shouldExpand ? actionButton(context).extended : actionButton(context).normal,
),
],
Expanded(
child: Column(
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
...widget.destinations.mapIndexed(
(index, destination) => CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
destination.label,
style: Theme.of(context).textTheme.titleSmall,
),
position: TooltipPosition.right,
child: view.toNavigationButton(
selected,
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),
),
),
customIcon: usePostersForLibrary
? ClipRRect(
borderRadius: FladderTheme.smallShape.borderRadius,
child: SizedBox.square(
dimension: 50,
child: FladderImage(
image: view.imageData?.primary,
placeHolder: Card(
child: Icon(
selected
? view.collectionType.icon
: view.collectionType.iconOutlined,
),
position: TooltipPosition.right,
child: 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 selected = context.router.currentUrl.contains(view.id);
final actions = [
ItemActionButton(
label: Text(context.localized.scanLibrary),
icon: const Icon(IconsaxPlusLinear.refresh),
action: () => showRefreshPopup(context, view.id, view.name),
)
];
return CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
view.name,
style: Theme.of(context).textTheme.titleSmall,
),
),
),
)
: null,
trailing: actions,
),
);
},
).toList(),
builder: (context, remaining) {
return CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
context.localized.moreOptions,
style: Theme.of(context).textTheme.titleSmall,
position: TooltipPosition.right,
child: view.toNavigationButton(
selected,
true,
shouldExpand,
() => context.pushRoute(
LibrarySearchRoute(
viewModelId: view.id,
).withFilter(view.collectionType.defaultFilters),
),
onLongPress: () => showBottomSheetPill(
context: context,
content: (context, scrollController) => ListView(
shrinkWrap: true,
controller: scrollController,
children: actions.listTileItems(context, useIcons: true),
),
),
customIcon: usePostersForLibrary
? ClipRRect(
borderRadius: FladderTheme.smallShape.borderRadius,
child: SizedBox.square(
dimension: 50,
child: FladderImage(
image: view.imageData?.primary,
placeHolder: Card(
child: Icon(
selected
? view.collectionType.icon
: view.collectionType.iconOutlined,
),
),
),
),
)
: null,
trailing: actions,
),
position: TooltipPosition.right,
child: PopupMenuButton(
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
padding: EdgeInsets.zero,
tooltip: "",
icon: NavigationButton(
label: context.localized.other,
selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down),
icon: const Icon(IconsaxPlusLinear.arrow_square_down),
expanded: shouldExpand,
customIcon: usePostersForLibrary
? ClipRRect(
borderRadius: FladderTheme.smallShape.borderRadius,
child: const SizedBox.square(
dimension: 50,
child: Card(
child: Icon(IconsaxPlusLinear.arrow_square_down),
);
},
).toList(),
builder: (context, remaining) {
return CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
context.localized.moreOptions,
style: Theme.of(context).textTheme.titleSmall,
),
),
)
: null,
horizontal: true,
),
itemBuilder: (context) => views
.sublist(views.length - remaining)
.map(
(e) => PopupMenuItem(
onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)),
child: Row(
spacing: 8,
children: [
usePostersForLibrary
? Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: ClipRRect(
borderRadius: FladderTheme.smallShape.borderRadius,
child: SizedBox.square(
dimension: 45,
child: FladderImage(
image: e.imageData?.primary,
placeHolder: Card(
child: Icon(
e.collectionType.iconOutlined,
),
position: TooltipPosition.right,
child: PopupMenuButton(
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
padding: EdgeInsets.zero,
tooltip: "",
icon: NavigationButton(
label: context.localized.other,
selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down),
icon: const Icon(IconsaxPlusLinear.arrow_square_down),
expanded: shouldExpand,
customIcon: usePostersForLibrary
? ClipRRect(
borderRadius: FladderTheme.smallShape.borderRadius,
child: const SizedBox.square(
dimension: 50,
child: Card(
child: Icon(IconsaxPlusLinear.arrow_square_down),
),
),
)
: null,
horizontal: true,
),
itemBuilder: (context) => views
.sublist(views.length - remaining)
.map(
(e) => PopupMenuItem(
onTap: () => context.pushRoute(LibrarySearchRoute(
viewModelId: e.id,
).withFilter(e.collectionType.defaultFilters)),
child: Row(
spacing: 8,
children: [
usePostersForLibrary
? Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: ClipRRect(
borderRadius: FladderTheme.smallShape.borderRadius,
child: SizedBox.square(
dimension: 45,
child: FladderImage(
image: e.imageData?.primary,
placeHolder: Card(
child: Icon(
e.collectionType.iconOutlined,
),
),
),
),
),
),
),
)
: Icon(e.collectionType.iconOutlined),
Text(e.name),
],
),
),
)
.toList(),
),
);
},
),
),
],
],
),
)
: 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 SettingsUserIcon(),
onPressed: () {
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
context.router.push(const SettingsRoute());
} else {
context.router.push(const ClientSettingsRoute());
}
},
),
],
),
NavigationButton(
label: context.localized.settings,
selected: widget.currentLocation.contains(const SettingsRoute().routeName),
selectedIcon: const Icon(IconsaxPlusBold.setting_3),
horizontal: true,
expanded: shouldExpand,
icon: const SettingsUserIcon(),
onPressed: () {
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
context.router.push(const SettingsRoute());
} else {
context.router.push(const ClientSettingsRoute());
}
},
),
],
),
),
),
),

View file

@ -8,6 +8,7 @@ import 'package:fladder/providers/connectivity_provider.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/home_screen.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';
@ -77,6 +78,9 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
final calculatedBottomViewPadding =
showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding;
final currentTab =
HomeTabs.values.elementAtOrNull(currentIndex.clamp(0, HomeTabs.values.length - 1)) ?? HomeTabs.dashboard;
return PopScope(
canPop: currentIndex == 0,
onPopInvokedWithResult: (didPop, result) {
@ -124,7 +128,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
hiddenHeight: calculatedBottomViewPadding,
duration: const Duration(milliseconds: 250),
child: HideOnScroll(
controller: AdaptiveLayout.scrollOf(context),
controller: AdaptiveLayout.scrollOf(context, currentTab),
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
child: NestedBottomAppBar(
child: SizedBox(

View file

@ -29,7 +29,7 @@ class HideOnScroll extends ConsumerStatefulWidget {
}
class _HideOnScrollState extends ConsumerState<HideOnScroll> {
late final ScrollController scrollController = widget.controller ?? ScrollController();
late ScrollController scrollController = widget.controller ?? ScrollController();
bool isVisible = true;
@override
@ -47,6 +47,16 @@ class _HideOnScrollState extends ConsumerState<HideOnScroll> {
super.dispose();
}
@override
void didUpdateWidget(covariant HideOnScroll oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
scrollController.removeListener(_onScroll);
scrollController = widget.controller ?? ScrollController();
scrollController.addListener(_onScroll);
}
}
void _onScroll() {
if (!widget.canHide) {
if (!isVisible) {

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -19,6 +21,7 @@ class HorizontalList<T> extends ConsumerStatefulWidget {
final Widget Function(BuildContext context, int index) itemBuilder;
final bool scrollToEnd;
final EdgeInsets contentPadding;
final double? dominantRatio;
final double? height;
final bool shrinkWrap;
const HorizontalList({
@ -33,6 +36,7 @@ class HorizontalList<T> extends ConsumerStatefulWidget {
this.contentPadding = const EdgeInsets.symmetric(horizontal: 16),
this.subtext,
this.shrinkWrap = false,
this.dominantRatio,
super.key,
});
@ -201,9 +205,11 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
),
const SizedBox(height: 8),
SizedBox(
height: (widget.height ??
AdaptiveLayout.poster(context).size *
ref.watch(clientSettingsProvider.select((value) => value.posterSize))),
height: widget.height ??
((AdaptiveLayout.poster(context).size *
ref.watch(clientSettingsProvider.select((value) => value.posterSize))) /
pow((widget.dominantRatio ?? 1.0), 0.55)) *
0.72,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,

View file

@ -158,7 +158,7 @@ class _ProgressFloatingButtonState extends ConsumerState<ProgressFloatingButton>
}
: null,
child: FloatingActionButton(
heroTag: null,
heroTag: "Progress_Floating_Button",
onPressed: isActive ? timer.cancel : timer.play,
child: Stack(
fit: StackFit.expand,

View file

@ -14,12 +14,16 @@ class SelectableIconButton extends ConsumerStatefulWidget {
final IconData icon;
final IconData? selectedIcon;
final bool selected;
final Color? backgroundColor;
final Color? iconColor;
const SelectableIconButton({
required this.onPressed,
required this.selected,
required this.icon,
this.selectedIcon,
this.label,
this.backgroundColor,
this.iconColor,
super.key,
});
@ -37,9 +41,14 @@ class _SelectableIconButtonState extends ConsumerState<SelectableIconButton> {
message: widget.label ?? "",
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.primary) : null,
iconColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary) : null,
foregroundColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary) : null,
elevation: WidgetStatePropertyAll(
widget.backgroundColor != null ? (widget.backgroundColor!.a < 1 ? 0 : null) : null),
backgroundColor: WidgetStatePropertyAll(
widget.backgroundColor ?? (widget.selected ? Theme.of(context).colorScheme.primary : null)),
iconColor: WidgetStatePropertyAll(
widget.iconColor ?? (widget.selected ? Theme.of(context).colorScheme.onPrimary : null)),
foregroundColor: WidgetStatePropertyAll(
widget.iconColor ?? (widget.selected ? Theme.of(context).colorScheme.onPrimary : null)),
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
),
onPressed: loading