mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
Improvements to side navigation bar
Use custom tooltip instead of auto expanding sidebar
This commit is contained in:
parent
fd3c97a214
commit
82e09b3e0c
7 changed files with 210 additions and 68 deletions
|
|
@ -181,7 +181,6 @@ class FladderCupertinoLocalizationsDelegate extends LocalizationsDelegate<Cupert
|
||||||
dayFormat = intl.DateFormat.d(correctedLocale);
|
dayFormat = intl.DateFormat.d(correctedLocale);
|
||||||
weekdayFormat = intl.DateFormat.E(correctedLocale);
|
weekdayFormat = intl.DateFormat.E(correctedLocale);
|
||||||
mediumDateFormat = intl.DateFormat.MMMEd(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);
|
singleDigitHourFormat = intl.DateFormat('HH', correctedLocale);
|
||||||
singleDigitMinuteFormat = intl.DateFormat.m(correctedLocale);
|
singleDigitMinuteFormat = intl.DateFormat.m(correctedLocale);
|
||||||
doubleDigitMinuteFormat = intl.DateFormat('mm', correctedLocale);
|
doubleDigitMinuteFormat = intl.DateFormat('mm', correctedLocale);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,15 @@ class _DefaultTitleBarState extends ConsumerState<DefaultTitleBar> with WindowLi
|
||||||
onExit: (event) => setState(() => hovering = false),
|
onExit: (event) => setState(() => hovering = false),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 250),
|
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,
|
height: widget.height,
|
||||||
child: kIsWeb
|
child: kIsWeb
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ class NestedScaffold extends ConsumerWidget {
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.98),
|
Theme.of(context).colorScheme.surface.withValues(alpha: 0.85),
|
||||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.8),
|
Theme.of(context).colorScheme.surface.withValues(alpha: 0.7),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
return NavigationButton(
|
||||||
label: label,
|
label: label,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onPressed: action,
|
onPressed: action,
|
||||||
horizontal: horizontal,
|
horizontal: horizontal,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
|
customIcon: customIcon,
|
||||||
selectedIcon: selectedIcon!,
|
selectedIcon: selectedIcon!,
|
||||||
icon: icon!,
|
icon: icon!,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: widget.customIcon != null
|
padding: widget.customIcon != null
|
||||||
? EdgeInsetsGeometry.zero
|
? EdgeInsetsGeometry.zero
|
||||||
: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: widget.customIcon != null ? 60 : 35,
|
height: widget.customIcon != null ? 60 : 35,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.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/destination_model.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.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/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/item_actions.dart';
|
||||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||||
|
|
||||||
|
|
@ -46,26 +45,6 @@ class SideNavigationBar extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
bool expandedSideBar = false;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -78,7 +57,7 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
final collapsedWidth = 90 + padding.left;
|
final collapsedWidth = 90 + padding.left;
|
||||||
final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||||
final fullyExpanded = largeBar ? expandedSideBar : false;
|
final fullyExpanded = largeBar ? expandedSideBar : false;
|
||||||
final shouldExpand = showOnHover || fullyExpanded;
|
final shouldExpand = fullyExpanded;
|
||||||
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
|
|
@ -93,9 +72,6 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
|
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
|
||||||
width: shouldExpand ? expandedWidth : collapsedWidth,
|
width: shouldExpand ? expandedWidth : collapsedWidth,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (value) => startTimer(),
|
|
||||||
onExit: (event) => stopTimer(),
|
|
||||||
onHover: (value) => startTimer(),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: padding.top),
|
SizedBox(height: padding.top),
|
||||||
|
|
@ -119,12 +95,7 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: !largeBar
|
onPressed: !largeBar
|
||||||
? () => widget.scaffoldKey.currentState?.openDrawer()
|
? () => widget.scaffoldKey.currentState?.openDrawer()
|
||||||
: () => setState(() {
|
: () => setState(() => expandedSideBar = !expandedSideBar),
|
||||||
expandedSideBar = !expandedSideBar;
|
|
||||||
if (!expandedSideBar) {
|
|
||||||
showOnHover = false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
largeBar && expandedSideBar
|
largeBar && expandedSideBar
|
||||||
? IconsaxPlusLinear.sidebar_left
|
? IconsaxPlusLinear.sidebar_left
|
||||||
|
|
@ -147,9 +118,27 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
spacing: 2,
|
spacing: 2,
|
||||||
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
|
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
...widget.destinations.mapIndexed(
|
...widget.destinations.mapIndexed(
|
||||||
(index, destination) =>
|
(index, destination) => CustomTooltip(
|
||||||
destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand),
|
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) ...[
|
if (views.isNotEmpty && largeBar) ...[
|
||||||
const Divider(
|
const Divider(
|
||||||
|
|
@ -170,7 +159,20 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
action: () => showRefreshPopup(context, view.id, view.name),
|
action: () => showRefreshPopup(context, view.id, view.name),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
return view.toNavigationButton(
|
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,
|
selected,
|
||||||
true,
|
true,
|
||||||
shouldExpand,
|
shouldExpand,
|
||||||
|
|
@ -202,6 +204,7 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
trailing: actions,
|
trailing: actions,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
|
|
|
||||||
131
lib/widgets/shared/custom_tooltip.dart
Normal file
131
lib/widgets/shared/custom_tooltip.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue