302 lines
14 KiB
Dart
302 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
// --- Data Structure for Sidebar Menu Items ---
|
|
class SidebarItem {
|
|
final IconData? icon; // Now nullable, as we might use an image
|
|
final String label;
|
|
final String? route; // Null if it's a parent category
|
|
final List<SidebarItem>? children; // Sub-items
|
|
final bool isParent;
|
|
final String? imagePath; // New: Optional path to an image asset
|
|
|
|
SidebarItem({
|
|
this.icon,
|
|
required this.label,
|
|
this.route,
|
|
this.children,
|
|
this.isParent = false,
|
|
this.imagePath, // Initialize the new property
|
|
}) : assert(icon != null || imagePath != null, 'Either icon or imagePath must be provided'); // Ensure one is present
|
|
}
|
|
|
|
// --- Collapsible Sidebar Widget ---
|
|
class CollapsibleSidebar extends StatefulWidget {
|
|
final Function(String route) onNavigate;
|
|
final bool isCollapsed; // Receive collapse state from HomePage
|
|
final VoidCallback onToggle; // Receive toggle callback from HomePage (for AppBar button)
|
|
|
|
const CollapsibleSidebar({
|
|
super.key,
|
|
required this.onNavigate,
|
|
required this.isCollapsed,
|
|
required this.onToggle,
|
|
});
|
|
|
|
@override
|
|
State<CollapsibleSidebar> createState() => _CollapsibleSidebarState();
|
|
}
|
|
|
|
class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
|
|
// Define your menu items here based on the routes you provided
|
|
late final List<SidebarItem> _menuItems;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_menuItems = [
|
|
SidebarItem(
|
|
// Reverted to IconData for the top-level 'Air' category
|
|
icon: Icons.cloud,
|
|
label: "Air",
|
|
isParent: true,
|
|
children: [
|
|
// Added Manual sub-category for Air
|
|
SidebarItem(
|
|
icon: Icons.handshake, // Example icon for Manual
|
|
label: "Manual",
|
|
isParent: true,
|
|
children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/manual/dashboard'),
|
|
SidebarItem(icon: Icons.edit_note, label: "Manual Sampling", route: '/air/manual/manual-sampling'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/manual/report'),
|
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/air/manual/data-log'),
|
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/air/manual/image-request'),
|
|
],
|
|
),
|
|
SidebarItem(
|
|
icon: Icons.trending_up, label: "Continuous", isParent: true, children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/continuous/dashboard'),
|
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
|
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
|
|
]),
|
|
SidebarItem(
|
|
icon: Icons.search, label: "Investigative", isParent: true, children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/investigative/dashboard'),
|
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
|
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
|
|
]),
|
|
],
|
|
),
|
|
SidebarItem(
|
|
// Reverted to IconData for the top-level 'River' category
|
|
icon: Icons.water,
|
|
label: "River",
|
|
isParent: true,
|
|
children: [
|
|
// Added Manual sub-category for River
|
|
SidebarItem(
|
|
icon: Icons.handshake, // Example icon for Manual
|
|
label: "Manual",
|
|
isParent: true,
|
|
children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/manual/dashboard'),
|
|
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/river/manual/in-situ'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/manual/report'),
|
|
SidebarItem(icon: Icons.date_range, label: "Triennial Sampling", route: '/river/manual/triennial'),
|
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/river/manual/data-log'),
|
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/river/manual/image-request'),
|
|
],
|
|
),
|
|
SidebarItem(
|
|
icon: Icons.trending_up, label: "Continuous", isParent: true, children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/continuous/dashboard'),
|
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/continuous/overview'),
|
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/continuous/entry'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/continuous/report'),
|
|
]),
|
|
SidebarItem(
|
|
icon: Icons.search, label: "Investigative", isParent: true, children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/investigative/dashboard'),
|
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/investigative/overview'),
|
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/investigative/entry'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/investigative/report'),
|
|
]),
|
|
],
|
|
),
|
|
SidebarItem(
|
|
// Reverted to IconData for the top-level 'Marine' category
|
|
icon: Icons.sailing,
|
|
label: "Marine",
|
|
isParent: true,
|
|
children: [
|
|
// Added Manual sub-category for Marine
|
|
SidebarItem(
|
|
icon: Icons.handshake, // Example icon for Manual
|
|
label: "Manual",
|
|
isParent: true,
|
|
children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/manual/dashboard'),
|
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/marine/manual/info'),
|
|
SidebarItem(icon: Icons.assignment, label: "Pre-Sampling", route: '/marine/manual/pre-sampling'),
|
|
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/marine/manual/in-situ'),
|
|
SidebarItem(icon: Icons.waves, label: "Tarball Sampling", route: '/marine/manual/tarball'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
|
],
|
|
),
|
|
SidebarItem(
|
|
icon: Icons.trending_up, label: "Continuous", isParent: true, children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/continuous/dashboard'),
|
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/continuous/overview'),
|
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/continuous/entry'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/continuous/report'),
|
|
]),
|
|
SidebarItem(
|
|
icon: Icons.search, label: "Investigative", isParent: true, children: [
|
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/investigative/dashboard'),
|
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/investigative/overview'),
|
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/investigative/entry'),
|
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/investigative/report'),
|
|
]),
|
|
],
|
|
),
|
|
// Added Settings menu item
|
|
SidebarItem(icon: Icons.settings, label: "Settings", route: '/settings'),
|
|
];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: widget.isCollapsed ? 70 : 280, // Increased expanded width for sub-menus
|
|
color: Theme.of(context).primaryColor, // Use theme primary color for sidebar
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 16), // Spacing below the (now removed) toggle button area
|
|
|
|
Expanded(
|
|
child: ListView(
|
|
padding: EdgeInsets.zero, // Remove default listview padding
|
|
children: [
|
|
if (!widget.isCollapsed) ...[
|
|
// If sidebar is expanded, show full categories with sub-menus
|
|
for (var item in _menuItems)
|
|
_buildExpandableNavItem(item),
|
|
] else ...[
|
|
// If sidebar is collapsed, only show icons for top-level items
|
|
for (var item in _menuItems)
|
|
_buildCollapsedNavItem(item),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const Divider(color: Colors.white24, height: 1), // Separator before logout
|
|
// Logout item, using an icon as it's a standard action
|
|
_buildNavItem(Icons.logout, "Logout", '/logout', isTopLevel: true),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Helper to build the leading widget (Icon or Image) for a sidebar item
|
|
Widget _buildLeadingWidget(SidebarItem item) {
|
|
// Now only checks for icon, as imagePath is not used for top-level items anymore
|
|
// but the property still exists on SidebarItem for potential future use or other items.
|
|
return Icon(item.icon, color: Colors.white);
|
|
}
|
|
|
|
// Builds an expandable item for parent categories (only when sidebar is expanded)
|
|
Widget _buildExpandableNavItem(SidebarItem item) {
|
|
if (item.children == null || item.children!.isEmpty) {
|
|
// This case handles a top-level item that is NOT a parent,
|
|
// but if you have such an item, it should probably go through _buildNavItem directly.
|
|
// For this structure, all top-level items are parents.
|
|
return _buildNavItem(item.icon, item.label, item.route ?? '', isTopLevel: true, imagePath: item.imagePath);
|
|
}
|
|
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(dividerColor: Colors.transparent), // Hide divider in expansion tile
|
|
child: ExpansionTile(
|
|
initiallyExpanded: false, // You can set this to true if you want some categories open by default
|
|
leading: _buildLeadingWidget(item), // Use the helper for leading widget
|
|
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
|
iconColor: Colors.white,
|
|
collapsedIconColor: Colors.white,
|
|
childrenPadding: const EdgeInsets.only(left: 20.0), // Indent sub-items
|
|
children: item.children!.map((childItem) {
|
|
if (childItem.isParent) {
|
|
// Nested expansion tiles for sub-categories like "Manual", "Continuous"
|
|
return _buildExpandableNavItem(childItem);
|
|
} else {
|
|
// Leaf item (actual navigation link)
|
|
return _buildNavItem(childItem.icon, childItem.label, childItem.route ?? '', imagePath: childItem.imagePath);
|
|
}
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Builds a regular navigation item (for sub-items when expanded, or top-level when collapsed)
|
|
Widget _buildNavItem(IconData? icon, String label, String route, {bool isTopLevel = false, String? imagePath}) {
|
|
return InkWell(
|
|
onTap: () {
|
|
if (route.isNotEmpty) {
|
|
widget.onNavigate(route);
|
|
}
|
|
},
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 12,
|
|
horizontal: isTopLevel && widget.isCollapsed ? 0 : 8, // Center icon if top-level and collapsed
|
|
),
|
|
child: isTopLevel && widget.isCollapsed
|
|
? Center(
|
|
child: icon != null
|
|
? Icon(icon, color: Colors.white)
|
|
: const SizedBox.shrink(), // Fallback if no icon or imagePath
|
|
) // Only icon when collapsed
|
|
: Row(
|
|
children: [
|
|
icon != null
|
|
? Icon(icon, color: Colors.white)
|
|
: const SizedBox.shrink(), // Fallback if no icon or imagePath
|
|
const SizedBox(width: 12),
|
|
Expanded( // Use Expanded to prevent text overflow
|
|
child: Text(label, style: const TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Builds a collapsed navigation item (only icon/image) for top-level categories
|
|
Widget _buildCollapsedNavItem(SidebarItem item) {
|
|
return InkWell(
|
|
onTap: () {
|
|
if (item.route != null && item.route!.isNotEmpty) {
|
|
widget.onNavigate(item.route!);
|
|
} else {
|
|
// If a parent item is tapped when collapsed, expand the sidebar
|
|
widget.onToggle();
|
|
}
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 0), // Adjusted vertical padding
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min, // Use minimum space
|
|
children: [
|
|
item.icon != null
|
|
? Icon(item.icon, color: Colors.white, size: 24) // Icon size
|
|
: const SizedBox.shrink(), // Fallback if no icon
|
|
const SizedBox(height: 4), // Small space between icon and text
|
|
Text(
|
|
item.label,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10, // Smaller font size to fit
|
|
),
|
|
overflow: TextOverflow.ellipsis, // Handle long labels
|
|
maxLines: 1, // Ensure it stays on one line
|
|
textAlign: TextAlign.center, // Center the text
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|