feat: On hover of the Nav Bar, expand to show current icon tooltip text (#4976)

* feat: On hover of the Nav Bar, expand to show current icon tooltip text

* animate menu transition
This commit is contained in:
ggurdin 2025-12-30 13:24:28 -05:00 committed by GitHub
parent 86e83c80f2
commit dd9a4858d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 314 additions and 198 deletions

View file

@ -17,6 +17,7 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
final Color? backgroundColor;
final BorderRadius? borderRadius;
final bool expanded;
// Pangea#
const NaviRailItem({
@ -29,6 +30,7 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
this.backgroundColor,
this.borderRadius,
this.expanded = false,
// Pangea#
super.key,
});
@ -52,91 +54,118 @@ class NaviRailItem extends StatelessWidget {
// #Pangea
// return SizedBox(
// height: 72,
return SizedBox(
height: width - (isColumnMode ? 16.0 : 12.0),
width: width,
// width: FluffyThemes.navRailWidth,
// Pangea#
child: Stack(
children: [
Positioned(
top: 8,
bottom: 8,
left: 0,
child: AnimatedContainer(
width: isSelected
? FluffyThemes.isColumnMode(context)
? 8
: 4
: 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(90),
bottomRight: Radius.circular(90),
),
),
),
),
Center(
child: AnimatedScale(
scale: hovered ? 1.1 : 1.0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
// #Pangea
// child: Material(
// borderRadius: borderRadius,
// color: isSelected
// ? theme.colorScheme.primaryContainer
// : theme.colorScheme.surfaceContainerHigh,
child: UnreadRoomsBadge(
filter: unreadBadgeFilter ?? (_) => false,
badgePosition: BadgePosition.topEnd(
top: 1,
end: isColumnMode ? 8 : 4,
),
child: Container(
alignment: Alignment.center,
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: width - (isColumnMode ? 16.0 : 12.0),
width: width,
// width: FluffyThemes.navRailWidth,
// Pangea#
child: Stack(
children: [
Positioned(
top: 8,
bottom: 8,
left: 0,
child: AnimatedContainer(
width: isSelected
? FluffyThemes.isColumnMode(context)
? 8
: 4
: 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
decoration: BoxDecoration(
color: backgroundColor ??
(isSelected
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHigh),
borderRadius: borderRadius,
),
margin: EdgeInsets.symmetric(
horizontal: isColumnMode ? 16.0 : 12.0,
vertical: isColumnMode ? 8.0 : 6.0,
),
// Pangea#
child: Tooltip(
message: toolTip,
child: InkWell(
borderRadius: borderRadius,
onTap: onTap,
// #Pangea
child: icon,
// child: unreadBadgeFilter == null
// ? icon
// : UnreadRoomsBadge(
// filter: unreadBadgeFilter,
// badgePosition: BadgePosition.topEnd(
// top: -12,
// end: -8,
// ),
// child: icon,
// ),
// Pangea#
color: theme.colorScheme.primary,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(90),
bottomRight: Radius.circular(90),
),
),
),
),
Center(
child: AnimatedScale(
scale: hovered ? 1.1 : 1.0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
// #Pangea
// child: Material(
// borderRadius: borderRadius,
// color: isSelected
// ? theme.colorScheme.primaryContainer
// : theme.colorScheme.surfaceContainerHigh,
child: UnreadRoomsBadge(
filter: unreadBadgeFilter ?? (_) => false,
badgePosition: BadgePosition.topEnd(
top: 1,
end: isColumnMode ? 8 : 4,
),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: backgroundColor ??
(isSelected
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHigh),
borderRadius: borderRadius,
),
margin: EdgeInsets.symmetric(
horizontal: isColumnMode ? 16.0 : 12.0,
vertical: isColumnMode ? 8.0 : 6.0,
),
// Pangea#
child: Tooltip(
message: toolTip,
child: InkWell(
borderRadius: borderRadius,
onTap: onTap,
// #Pangea
child: icon,
// child: unreadBadgeFilter == null
// ? icon
// : UnreadRoomsBadge(
// filter: unreadBadgeFilter,
// badgePosition: BadgePosition.topEnd(
// top: -12,
// end: -8,
// ),
// child: icon,
// ),
// Pangea#
),
),
),
),
),
),
],
),
),
if (expanded)
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: ListTile(
title: Text(
toolTip,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium,
),
onTap: onTap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 0.0,
),
),
),
),
],
),
],
);
},
);

View file

@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
import 'package:fluffychat/pangea/analytics_summary/level_analytics_details_content.dart';
import 'package:fluffychat/pangea/spaces/space_constants.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
class SpaceNavigationColumn extends StatefulWidget {
final GoRouterState state;
const SpaceNavigationColumn({
required this.state,
super.key,
});
@override
State<SpaceNavigationColumn> createState() => SpaceNavigationColumnState();
}
class SpaceNavigationColumnState extends State<SpaceNavigationColumn> {
bool _expand = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
bool showNavRail = isColumnMode;
if (!showNavRail) {
final roomID = widget.state.pathParameters['roomid'];
final spaceID = widget.state.pathParameters['spaceid'];
if (roomID == null && spaceID == null) {
showNavRail = !["newcourse", ":construct"].any(
(p) => widget.state.fullPath?.contains(p) ?? false,
);
} else if (roomID == null) {
showNavRail = widget.state.fullPath?.endsWith(':spaceid') == true;
}
}
final navRailWidth = isColumnMode
? FluffyThemes.navRailWidth
: FluffyThemes.navRailWidth - 8.0;
final double navRailExtraWidth = showNavRail ? 250.0 : 0.0;
final double columnWidth =
isColumnMode ? FluffyThemes.columnWidth + 1.0 : 0;
final double railWidth = showNavRail ? navRailWidth + 1.0 : 0;
final double baseWidth = columnWidth + railWidth;
final double expandedWidth = baseWidth < navRailExtraWidth
? navRailExtraWidth + railWidth
: baseWidth;
return AnimatedContainer(
duration: FluffyThemes.animationDuration,
width: _expand ? expandedWidth : baseWidth,
child: Stack(
children: [
if (isColumnMode)
Positioned(
left: navRailWidth + 1.0,
child: SizedBox(
height: MediaQuery.heightOf(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(),
width: FluffyThemes.columnWidth,
child: _MainView(state: widget.state),
),
Container(
width: 1.0,
color: theme.dividerColor,
),
],
),
),
),
if (showNavRail)
HoverBuilder(
builder: (context, hovered) {
if (_expand != hovered) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_expand = hovered;
});
});
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SpacesNavigationRail(
activeSpaceId: widget.state.pathParameters['spaceid'],
path: widget.state.fullPath,
railWidth: _expand
? navRailWidth + navRailExtraWidth
: navRailWidth,
expanded: _expand,
),
Container(
width: 1,
color: Theme.of(context).dividerColor,
),
],
);
},
),
],
),
);
}
}
class _MainView extends StatelessWidget {
final GoRouterState state;
const _MainView({
required this.state,
});
@override
Widget build(BuildContext context) {
final path = state.fullPath;
if (path == null) {
return ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.pathParameters['spaceid'],
);
}
if (path.contains("analytics")) {
if (path.contains("analytics/level")) {
return const LevelAnalyticsDetailsContent();
} else if (path.contains("analytics/activities")) {
return const ActivityArchive();
} else if (path.contains("analytics/${ConstructTypeEnum.morph.string}")) {
return const ConstructAnalyticsView(view: ConstructTypeEnum.morph);
}
return const ConstructAnalyticsView(view: ConstructTypeEnum.vocab);
}
if (path.contains("settings")) {
return Settings(key: state.pageKey);
}
if (path.contains('course')) {
return Center(
child: SizedBox(
width: 250.0,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${SpaceConstants.sideBearFileName}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
);
}
return ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.pathParameters['spaceid'],
);
}
}

View file

@ -1,18 +1,8 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
import 'package:fluffychat/pangea/analytics_summary/level_analytics_details_content.dart';
import 'package:fluffychat/pangea/spaces/space_constants.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
import 'package:fluffychat/pangea/spaces/space_navigation_column.dart';
class TwoColumnLayout extends StatelessWidget {
// #Pangea
@ -31,123 +21,27 @@ class TwoColumnLayout extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// #Pangea
bool showNavRail = FluffyThemes.isColumnMode(context);
if (!showNavRail) {
final roomID = state.pathParameters['roomid'];
final spaceID = state.pathParameters['spaceid'];
if (roomID == null && spaceID == null) {
showNavRail = !["newcourse", ":construct"].any(
(p) => state.fullPath?.contains(p) ?? false,
);
} else if (roomID == null) {
showNavRail = state.fullPath?.endsWith(':spaceid') == true;
}
}
// final theme = Theme.of(context);
// Pangea#
return ScaffoldMessenger(
child: Scaffold(
body: Row(
children: [
// #Pangea
if (showNavRail) ...[
SpacesNavigationRail(
activeSpaceId: state.pathParameters['spaceid'],
path: state.fullPath,
),
Container(
color: Theme.of(context).dividerColor,
width: 1,
),
],
if (FluffyThemes.isColumnMode(context)) ...[
// Pangea#
Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(),
// #Pangea
// width: FluffyThemes.columnWidth + FluffyThemes.navRailWidth,
// child: mainView,
width: FluffyThemes.columnWidth,
child: _MainView(state: state),
// Pangea#
),
Container(
width: 1.0,
color: theme.dividerColor,
),
// Pangea#
],
SpaceNavigationColumn(state: state),
// Container(
// clipBehavior: Clip.antiAlias,
// decoration: const BoxDecoration(),
// width: FluffyThemes.columnWidth + FluffyThemes.navRailWidth,
// child: mainView,
// ),
// Container(width: 1.0, color: theme.dividerColor),
// Pangea#
Expanded(
child: ClipRRect(
child: sideView,
),
),
Expanded(child: ClipRRect(child: sideView)),
],
),
),
);
}
}
// #Pangea
class _MainView extends StatelessWidget {
final GoRouterState state;
const _MainView({
required this.state,
});
@override
Widget build(BuildContext context) {
final path = state.fullPath;
if (path == null) {
return ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.pathParameters['spaceid'],
);
}
if (path.contains("analytics")) {
if (path.contains("analytics/level")) {
return const LevelAnalyticsDetailsContent();
} else if (path.contains("analytics/activities")) {
return const ActivityArchive();
} else if (path.contains("analytics/${ConstructTypeEnum.morph.string}")) {
return const ConstructAnalyticsView(view: ConstructTypeEnum.morph);
}
return const ConstructAnalyticsView(view: ConstructTypeEnum.vocab);
}
if (path.contains("settings")) {
return Settings(key: state.pageKey);
}
if (path.contains('course')) {
return Center(
child: SizedBox(
width: 250.0,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${SpaceConstants.sideBearFileName}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
);
}
return ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.pathParameters['spaceid'],
);
}
}
// Pangea#

View file

@ -22,6 +22,8 @@ class SpacesNavigationRail extends StatelessWidget {
// final void Function() onGoToChats;
// final void Function(String) onGoToSpaceId;
final String? path;
final double railWidth;
final bool expanded;
// Pangea#
const SpacesNavigationRail({
@ -30,6 +32,8 @@ class SpacesNavigationRail extends StatelessWidget {
// required this.onGoToChats,
// required this.onGoToSpaceId,
required this.path,
required this.railWidth,
this.expanded = false,
// Pangea#
super.key,
});
@ -73,10 +77,12 @@ class SpacesNavigationRail extends StatelessWidget {
)
.toList();
return SizedBox(
// #Pangea
// #Pangea
// return SizedBox(
return AnimatedContainer(
// width: FluffyThemes.navRailWidth,
width: width,
width: railWidth,
duration: FluffyThemes.animationDuration,
// Pangea#
child: Column(
children: [
@ -118,6 +124,9 @@ class SpacesNavigationRail extends StatelessWidget {
),
),
toolTip: L10n.of(context).home,
// #Pangea
expanded: expanded,
// Pangea#
);
}
i--;
@ -147,6 +156,7 @@ class SpacesNavigationRail extends StatelessWidget {
toolTip: L10n.of(context).directMessages,
unreadBadgeFilter: (room) =>
room.firstSpaceParent == null,
expanded: expanded,
// Pangea#
);
}
@ -183,6 +193,7 @@ class SpacesNavigationRail extends StatelessWidget {
),
),
toolTip: L10n.of(context).addCourse,
expanded: expanded,
// Pangea#
);
}
@ -260,6 +271,7 @@ class SpacesNavigationRail extends StatelessWidget {
),
),
),
expanded: expanded,
// Pangea#
);
},
@ -279,6 +291,7 @@ class SpacesNavigationRail extends StatelessWidget {
// ),
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
expanded: expanded,
// Pangea#
toolTip: L10n.of(context).settings,
),