diff --git a/lib/localization_delegates.dart b/lib/localization_delegates.dart index 55beb90..ec21bbe 100644 --- a/lib/localization_delegates.dart +++ b/lib/localization_delegates.dart @@ -181,7 +181,6 @@ class FladderCupertinoLocalizationsDelegate extends LocalizationsDelegate 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() diff --git a/lib/screens/shared/nested_scaffold.dart b/lib/screens/shared/nested_scaffold.dart index 7971dc4..62fded7 100644 --- a/lib/screens/shared/nested_scaffold.dart +++ b/lib/screens/shared/nested_scaffold.dart @@ -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), ], ), ), diff --git a/lib/widgets/navigation_scaffold/components/destination_model.dart b/lib/widgets/navigation_scaffold/components/destination_model.dart index 7be11e9..28f53ca 100644 --- a/lib/widgets/navigation_scaffold/components/destination_model.dart +++ b/lib/widgets/navigation_scaffold/components/destination_model.dart @@ -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!, ); diff --git a/lib/widgets/navigation_scaffold/components/navigation_button.dart b/lib/widgets/navigation_scaffold/components/navigation_button.dart index 4b6ca60..95b83b4 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_button.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_button.dart @@ -68,7 +68,7 @@ class _NavigationButtonState extends ConsumerState { ? 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( diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index 2ea0d5c..be63d81 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -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 { 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 { 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 { 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 { 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 { 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 { 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(), diff --git a/lib/widgets/shared/custom_tooltip.dart b/lib/widgets/shared/custom_tooltip.dart new file mode 100644 index 0000000..f20e7e9 --- /dev/null +++ b/lib/widgets/shared/custom_tooltip.dart @@ -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 { + 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(); + } +}