Last active
July 9, 2025 16:30
-
-
Save Slozzyondul/e67e6a05261387f9e28a6edcbe13940a to your computer and use it in GitHub Desktop.
I am trying to reduce the spacing between the menu bar item when isCollapsed is true but still unable to figure out
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:io'; | |
| import 'package:flutter/gestures.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | |
| import 'package:go_router/go_router.dart'; | |
| import 'package:prototype1/src/frontend/routing/app_router.dart'; | |
| import 'package:image_picker/image_picker.dart'; | |
| class DesktopLayout extends ConsumerStatefulWidget { | |
| final Widget child; | |
| const DesktopLayout({super.key, required this.child}); | |
| @override | |
| ConsumerState<DesktopLayout> createState() => _DesktopLayoutState(); | |
| } | |
| class _DesktopLayoutState extends ConsumerState<DesktopLayout> { | |
| File? _selectedImage; | |
| bool _isCollapsed = false; | |
| bool _isInitialBuild = true; | |
| final ImagePicker _picker = ImagePicker(); | |
| bool _isHealthExpanded = false; | |
| bool _isSecurityExpanded = false; | |
| // Dynamic spacing based on collapsed state | |
| // Reduced spacing for collapsed state | |
| double get _menuItemSpacing => | |
| _isCollapsed ? 0.0 : 2.0; // Changed from 2.0 to 0.0 or a very small value | |
| Future<void> _pickImage() async { | |
| final XFile? pickedFile = | |
| await _picker.pickImage(source: ImageSource.gallery); | |
| if (pickedFile != null) { | |
| setState(() { | |
| _selectedImage = File(pickedFile.path); | |
| }); | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar(content: Text('Profile image selected: ${pickedFile.name}')), | |
| ); | |
| } | |
| } | |
| } | |
| @override | |
| void didChangeDependencies() { | |
| super.didChangeDependencies(); | |
| if (_isInitialBuild && mounted) { | |
| final screenWidth = MediaQuery.of(context).size.width; | |
| if (screenWidth < 900) { | |
| setState(() { | |
| _isCollapsed = true; | |
| }); | |
| } | |
| _isInitialBuild = false; | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final currentLocation = GoRouterState.of(context).uri.path; | |
| return Material( | |
| child: Row( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| // Sidebar Navigation | |
| Material( | |
| elevation: 2, | |
| child: AnimatedContainer( | |
| duration: const Duration(milliseconds: 300), | |
| width: _isCollapsed ? 80 : 250, | |
| color: Theme.of(context).colorScheme.surfaceContainerHighest, | |
| child: ConstrainedBox( | |
| constraints: BoxConstraints( | |
| maxHeight: MediaQuery.of(context).size.height, | |
| ), | |
| child: SingleChildScrollView( | |
| primary: false, | |
| dragStartBehavior: DragStartBehavior.down, | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| crossAxisAlignment: _isCollapsed | |
| ? CrossAxisAlignment.center | |
| : CrossAxisAlignment.stretch, | |
| children: [ | |
| Padding( | |
| padding: EdgeInsets.all(_isCollapsed ? 2.0 : 4.0), | |
| child: IconButton( | |
| icon: | |
| Icon(_isCollapsed ? Icons.menu_open : Icons.menu), | |
| tooltip: _isCollapsed | |
| ? 'Expand Sidebar' | |
| : 'Collapse Sidebar', | |
| onPressed: () { | |
| setState(() { | |
| _isCollapsed = !_isCollapsed; | |
| }); | |
| }, | |
| ), | |
| ), | |
| AnimatedSwitcher( | |
| duration: Duration(milliseconds: 200), | |
| transitionBuilder: | |
| (Widget child, Animation<double> animation) { | |
| return FadeTransition( | |
| opacity: animation, | |
| child: ScaleTransition( | |
| scale: animation, | |
| child: child, | |
| ), | |
| ); | |
| }, | |
| child: _isCollapsed | |
| ? SizedBox.shrink(key: ValueKey('collapsed_avatar')) | |
| : Column( | |
| key: ValueKey('expanded_avatar'), | |
| children: [ | |
| GestureDetector( | |
| onTap: _pickImage, | |
| child: CircleAvatar( | |
| radius: 40, | |
| backgroundColor: | |
| Theme.of(context).colorScheme.primary, | |
| backgroundImage: _selectedImage != null | |
| ? FileImage(_selectedImage!) | |
| : null, | |
| child: _selectedImage == null | |
| ? Icon(Icons.person_add_alt_1, | |
| size: 40, | |
| color: Theme.of(context) | |
| .colorScheme | |
| .onPrimary) | |
| : null, | |
| ), | |
| ), | |
| SizedBox(height: _menuItemSpacing * 0), | |
| ], | |
| ), | |
| ), | |
| // Menu items with dynamic spacing | |
| ...[ | |
| buildNavigationItems( | |
| AppRoute.home, | |
| context, | |
| 'Home', | |
| Icons.home, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.attendance, | |
| context, | |
| 'Attendance', | |
| Icons.check_circle, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/attendance', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.onlineAssessments, | |
| context, | |
| 'Online Assessment', | |
| Icons.assessment, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/onlineAssessments', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.recordGrades, | |
| context, | |
| 'Record Grades', | |
| Icons.input, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/recordGrades', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.communicationNotifications, | |
| context, | |
| 'Communication & Notifications', | |
| Icons.notifications, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == | |
| '/communicationNotifications'), | |
| buildNavigationItems( | |
| AppRoute.transport, | |
| context, | |
| 'Transport', | |
| Icons.motorcycle, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/transport', | |
| ), | |
| // Health section | |
| _isCollapsed | |
| ? PopupMenuButton<String>( | |
| icon: Icon(Icons.health_and_safety, | |
| color: Theme.of(context).iconTheme.color), | |
| tooltip: 'Health', | |
| onSelected: (value) { | |
| if (value == 'medicalInventory') { | |
| context.go('/medicalInventory'); | |
| } | |
| }, | |
| itemBuilder: (context) => [ | |
| PopupMenuItem<String>( | |
| value: 'medicalInventory', | |
| child: Row( | |
| children: [ | |
| Icon(Icons.medical_information, | |
| size: 20), | |
| SizedBox(width: 8), | |
| Text('Medical Inventory'), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ) | |
| : Theme( | |
| data: Theme.of(context) | |
| .copyWith(dividerColor: Colors.transparent), | |
| child: ExpansionTile( | |
| initiallyExpanded: _isHealthExpanded, | |
| onExpansionChanged: (expanded) { | |
| setState(() { | |
| _isHealthExpanded = expanded; | |
| }); | |
| }, | |
| leading: Icon(Icons.health_and_safety, | |
| color: Theme.of(context).iconTheme.color), | |
| title: Text('Health', | |
| style: Theme.of(context) | |
| .textTheme | |
| .bodyLarge), | |
| trailing: Icon(_isHealthExpanded | |
| ? Icons.expand_less | |
| : Icons.expand_more), | |
| childrenPadding: EdgeInsets.only(left: 32.0), | |
| children: [ | |
| buildNavigationItems( | |
| AppRoute.medicalInventory, | |
| context, | |
| 'Medical Inventory', | |
| Icons.medical_information, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == | |
| '/medicalInventory', | |
| ), | |
| ], | |
| ), | |
| ), | |
| buildNavigationItems( | |
| AppRoute.finance, | |
| context, | |
| 'Finance', | |
| Icons.attach_money, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/finance', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.viewStudent, | |
| context, | |
| 'View Student', | |
| Icons.people_alt_outlined, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/viewStudent', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.teachers, | |
| context, | |
| 'Teachers', | |
| Icons.people, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/teachers', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.settings, | |
| context, | |
| 'Settings', | |
| Icons.settings, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/settings', | |
| ), | |
| // Security section | |
| _isCollapsed | |
| ? PopupMenuButton<String>( | |
| icon: Icon(Icons.security, | |
| color: Theme.of(context).iconTheme.color), | |
| tooltip: 'Security', | |
| onSelected: (value) { | |
| if (value == 'userManagement') { | |
| context.go('/userManagement'); | |
| } else if (value == 'userPermmisions') { | |
| context.go('/userPermmisions'); | |
| } | |
| }, | |
| itemBuilder: (context) => [ | |
| PopupMenuItem<String>( | |
| value: 'userManagement', | |
| child: Row( | |
| children: [ | |
| Icon(Icons.supervised_user_circle, | |
| size: 20), | |
| SizedBox(width: 8), | |
| Text('User Management'), | |
| ], | |
| ), | |
| ), | |
| PopupMenuItem<String>( | |
| value: 'userPermmisions', | |
| child: Row( | |
| children: [ | |
| Icon(Icons.perm_identity, size: 20), | |
| SizedBox(width: 8), | |
| Text('User Permmisions'), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ) | |
| : Theme( | |
| data: Theme.of(context) | |
| .copyWith(dividerColor: Colors.transparent), | |
| child: ExpansionTile( | |
| initiallyExpanded: _isSecurityExpanded, | |
| onExpansionChanged: (expanded) { | |
| setState(() { | |
| _isSecurityExpanded = expanded; | |
| }); | |
| }, | |
| leading: Icon(Icons.security, | |
| color: Theme.of(context).iconTheme.color), | |
| title: Text('Security', | |
| style: Theme.of(context) | |
| .textTheme | |
| .bodyLarge), | |
| trailing: Icon(_isSecurityExpanded | |
| ? Icons.expand_less | |
| : Icons.expand_more), | |
| childrenPadding: EdgeInsets.only(left: 32.0), | |
| children: [ | |
| buildNavigationItems( | |
| AppRoute.userManagement, | |
| context, | |
| 'User Management', | |
| Icons.supervised_user_circle, | |
| isCollapsed: _isCollapsed, | |
| isSelected: | |
| currentLocation == '/userManagement', | |
| ), | |
| buildNavigationItems( | |
| AppRoute.userPermmisions, | |
| context, | |
| 'User Permmisions', | |
| Icons.perm_identity, | |
| isCollapsed: _isCollapsed, | |
| isSelected: | |
| currentLocation == '/userPermmisions', | |
| ), | |
| ], | |
| ), | |
| ), | |
| buildNavigationItems( | |
| AppRoute.schoolConfiguration, | |
| context, | |
| 'School Configuration', | |
| Icons.confirmation_num, | |
| isCollapsed: _isCollapsed, | |
| isSelected: | |
| currentLocation == '/schoolConfiguration'), | |
| ] | |
| .expand((widget) => | |
| [widget, SizedBox(height: _menuItemSpacing)]) | |
| .toList() | |
| ..removeLast(), | |
| SizedBox(height: _menuItemSpacing * 2), | |
| buildNavigationItems( | |
| AppRoute.login, | |
| context, | |
| 'Login', | |
| Icons.logout, | |
| isCollapsed: _isCollapsed, | |
| isSelected: currentLocation == '/login', | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| // Main Content | |
| Expanded( | |
| child: SingleChildScrollView( | |
| child: Padding( | |
| padding: const EdgeInsets.all(32.0), | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| widget.child, | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| Widget buildNavigationItems( | |
| AppRoute route, | |
| BuildContext context, | |
| String label, | |
| IconData icon, { | |
| bool isSelected = false, | |
| bool isCollapsed = false, | |
| }) { | |
| if (isCollapsed) { | |
| Widget iconWidget = Icon( | |
| icon, | |
| color: isSelected | |
| ? Theme.of(context) | |
| .colorScheme | |
| .onPrimary // Use onPrimary for selected icon color on primary background | |
| : Theme.of(context) | |
| .colorScheme | |
| .onSurfaceVariant, // Use a suitable color for unselected icons | |
| ); | |
| Widget collapsedItem = Tooltip( | |
| message: label, | |
| waitDuration: const Duration(milliseconds: 300), | |
| child: InkWell( | |
| onTap: () => _navigateToRoute(context, route), | |
| borderRadius: BorderRadius.circular(8.0), | |
| // Hover, splash, and highlight colors should be set for the InkWell to show clearly | |
| hoverColor: Theme.of(context) | |
| .colorScheme | |
| .primary | |
| .withOpacity(0.1), // Lighter hover | |
| splashColor: Theme.of(context) | |
| .colorScheme | |
| .primary | |
| .withOpacity(0.2), // Lighter splash | |
| highlightColor: Theme.of(context) | |
| .colorScheme | |
| .primary | |
| .withOpacity(0.15), // Lighter highlight | |
| child: Container( | |
| height: 48.0, // Standard dense ListTile height for consistency | |
| alignment: Alignment.center, | |
| decoration: BoxDecoration( | |
| color: isSelected | |
| ? Theme.of(context) | |
| .colorScheme | |
| .primary // Use primary color for selected background | |
| : Colors.transparent, // Transparent when not selected | |
| borderRadius: BorderRadius.circular(8.0), | |
| ), | |
| child: iconWidget, | |
| ), | |
| ), | |
| ); | |
| return Padding( | |
| // *** MODIFICATION HERE *** | |
| // Reduced vertical padding for collapsed state | |
| padding: const EdgeInsets.symmetric( | |
| horizontal: 8.0, vertical: 2.0), // Reduced from 4.0 to 2.0 | |
| child: Material( | |
| // Material for InkWell to draw on, and for shape/clipping | |
| // The background color for the Material widget should typically be transparent | |
| // or match the sidebar's background if you want a solid color behind it. | |
| // It's currently `Theme.of(context).colorScheme.tertiary`, which might | |
| // give an unexpected background for unselected items. Consider making it transparent. | |
| color: Colors.transparent, // Changed to transparent | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), | |
| clipBehavior: Clip.antiAlias, | |
| child: collapsedItem, | |
| ), | |
| ); | |
| } else { | |
| // Expanded state using ListTile | |
| return Padding( | |
| // Keep existing padding for expanded state | |
| padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), | |
| child: MouseRegion( | |
| cursor: SystemMouseCursors.click, | |
| child: ListTile( | |
| leading: Icon(icon), | |
| title: Text(label), | |
| selected: isSelected, | |
| onTap: () => _navigateToRoute(context, route), | |
| shape: RoundedRectangleBorder( | |
| borderRadius: BorderRadius.circular(8), | |
| ), | |
| contentPadding: | |
| const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| void _navigateToRoute(BuildContext context, AppRoute route) { | |
| if (context.mounted) { | |
| debugPrint('Navigating to: ${route.name}'); | |
| context.goNamed(route.name); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment