Improvements to side navigation bar

Use custom tooltip instead of auto expanding sidebar
This commit is contained in:
PartyDonut 2025-07-30 21:18:07 +02:00
parent fd3c97a214
commit 82e09b3e0c
7 changed files with 210 additions and 68 deletions

View file

@ -181,7 +181,6 @@ class FladderCupertinoLocalizationsDelegate extends LocalizationsDelegate<Cupert
dayFormat = intl.DateFormat.d(correctedLocale);
weekdayFormat = intl.DateFormat.E(correctedLocale);
mediumDateFormat = intl.DateFormat.MMMEd(correctedLocale);
// TODO(xster): fix when https://github.com/dart-lang/intl/issues/207 is resolved.
singleDigitHourFormat = intl.DateFormat('HH', correctedLocale);
singleDigitMinuteFormat = intl.DateFormat.m(correctedLocale);
doubleDigitMinuteFormat = intl.DateFormat('mm', correctedLocale);

View file

@ -44,7 +44,15 @@ class _DefaultTitleBarState extends ConsumerState<DefaultTitleBar> with WindowLi
onExit: (event) => setState(() => hovering = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
color: surfaceColor.withValues(alpha: hovering ? 0.15 : 0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
surfaceColor.withValues(alpha: 0.7),
surfaceColor.withValues(alpha: hovering ? 0.7 : 0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
)),
height: widget.height,
child: kIsWeb
? const SizedBox.shrink()

View file

@ -23,8 +23,8 @@ class NestedScaffold extends ConsumerWidget {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).colorScheme.surface.withValues(alpha: 0.98),
Theme.of(context).colorScheme.surface.withValues(alpha: 0.8),
Theme.of(context).colorScheme.surface.withValues(alpha: 0.85),
Theme.of(context).colorScheme.surface.withValues(alpha: 0.7),
],
),
),

View file

@ -81,13 +81,14 @@ class DestinationModel {
);
}
NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded) {
NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded, {Widget? customIcon}) {
return NavigationButton(
label: label,
selected: selected,
onPressed: action,
horizontal: horizontal,
expanded: expanded,
customIcon: customIcon,
selectedIcon: selectedIcon!,
icon: icon!,
);

View file

@ -68,7 +68,7 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
? Padding(
padding: widget.customIcon != null
? EdgeInsetsGeometry.zero
: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: SizedBox(
height: widget.customIcon != null ? 60 : 35,
child: Row(

View file

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
@ -22,6 +20,7 @@ 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/custom_tooltip.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
@ -46,26 +45,6 @@ class SideNavigationBar extends ConsumerStatefulWidget {
class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
bool expandedSideBar = false;
bool showOnHover = false;
Timer? timer;
void startTimer() {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 650), () {
setState(() {
showOnHover = true;
});
});
}
void stopTimer() {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 125), () {
setState(() {
showOnHover = false;
});
});
}
@override
Widget build(BuildContext context) {
@ -78,7 +57,7 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
final collapsedWidth = 90 + padding.left;
final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
final fullyExpanded = largeBar ? expandedSideBar : false;
final shouldExpand = showOnHover || fullyExpanded;
final shouldExpand = fullyExpanded;
final isDesktop = AdaptiveLayout.of(context).isDesktop;
return Stack(
@ -93,9 +72,6 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
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(),
onHover: (value) => startTimer(),
child: Column(
children: [
SizedBox(height: padding.top),
@ -119,12 +95,7 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
child: IconButton(
onPressed: !largeBar
? () => widget.scaffoldKey.currentState?.openDrawer()
: () => setState(() {
expandedSideBar = !expandedSideBar;
if (!expandedSideBar) {
showOnHover = false;
}
}),
: () => setState(() => expandedSideBar = !expandedSideBar),
icon: Icon(
largeBar && expandedSideBar
? IconsaxPlusLinear.sidebar_left
@ -147,9 +118,27 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
spacing: 2,
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
const SizedBox(height: 8),
...widget.destinations.mapIndexed(
(index, destination) =>
destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand),
(index, destination) => CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
destination.label,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
position: TooltipPosition.right,
child: destination.toNavigationButton(
widget.currentIndex == index,
true,
shouldExpand,
),
),
),
if (views.isNotEmpty && largeBar) ...[
const Divider(
@ -170,38 +159,52 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
action: () => showRefreshPopup(context, view.id, view.name),
)
];
return 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),
return CustomTooltip(
tooltipContent: expandedSideBar
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
view.name,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
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,
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,
)
: null,
trailing: actions,
),
);
},
).toList(),

View file

@ -0,0 +1,131 @@
import 'dart:async';
import 'package:flutter/material.dart';
class CustomTooltip extends StatefulWidget {
final Widget child;
final Widget? tooltipContent;
final double offset;
final TooltipPosition position;
final Duration showDelay;
const CustomTooltip({
required this.child,
required this.tooltipContent,
this.offset = 12,
this.position = TooltipPosition.top,
this.showDelay = const Duration(milliseconds: 125),
super.key,
});
@override
CustomTooltipState createState() => CustomTooltipState();
}
enum TooltipPosition { top, bottom, left, right }
class CustomTooltipState extends State<CustomTooltip> {
OverlayEntry? _overlayEntry;
Timer? _timer;
final GlobalKey _tooltipKey = GlobalKey();
void _showTooltip() {
_timer = Timer(widget.showDelay, () {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
});
}
void _hideTooltip() {
_timer?.cancel();
_overlayEntry?.remove();
_overlayEntry = null;
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset targetPosition = renderBox.localToGlobal(Offset.zero);
Size targetSize = renderBox.size;
WidgetsBinding.instance.addPostFrameCallback((_) {
final tooltipRenderBox = _tooltipKey.currentContext?.findRenderObject() as RenderBox?;
if (tooltipRenderBox != null) {
Size tooltipSize = tooltipRenderBox.size;
Offset tooltipPosition;
switch (widget.position) {
case TooltipPosition.top:
tooltipPosition = Offset(
targetPosition.dx + (targetSize.width - tooltipSize.width) / 2,
targetPosition.dy - tooltipSize.height - widget.offset,
);
break;
case TooltipPosition.bottom:
tooltipPosition = Offset(
targetPosition.dx + (targetSize.width - tooltipSize.width) / 2,
targetPosition.dy + targetSize.height + widget.offset,
);
break;
case TooltipPosition.left:
tooltipPosition = Offset(
targetPosition.dx - tooltipSize.width - widget.offset,
targetPosition.dy + (targetSize.height - tooltipSize.height) / 2,
);
break;
case TooltipPosition.right:
tooltipPosition = Offset(
targetPosition.dx + targetSize.width + widget.offset,
targetPosition.dy + (targetSize.height - tooltipSize.height) / 2,
);
break;
}
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: tooltipPosition.dx,
top: tooltipPosition.dy,
child: Material(
color: Colors.transparent,
child: widget.tooltipContent,
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
});
return OverlayEntry(
builder: (context) => const SizedBox.shrink(),
);
}
@override
Widget build(BuildContext context) {
if (widget.tooltipContent == null) return widget.child;
return MouseRegion(
onEnter: (_) => _showTooltip(),
onExit: (_) => _hideTooltip(),
child: Stack(
children: [
widget.child,
Positioned(
left: -1000,
top: -1000,
child: Container(
key: _tooltipKey,
child: widget.tooltipContent,
),
),
],
),
);
}
@override
void dispose() {
_timer?.cancel();
_overlayEntry?.remove();
super.dispose();
}
}