|
1 | 1 | import 'package:fluent_ui/fluent_ui.dart'; |
| 2 | +import 'package:fluentui_system_icons/fluentui_system_icons.dart' as msicons; |
2 | 3 | import 'package:go_router/go_router.dart'; |
3 | 4 | import 'package:revitool/i18n/generated/strings.g.dart'; |
4 | 5 |
|
5 | | -class AppRoutes { |
6 | | - static const String home = '/'; |
7 | | - static const String tweaks = '/tweaks'; |
8 | | - static const String msStore = '/msstore'; |
9 | | - static const String settings = '/settings'; |
| 6 | +enum RouteSection { main, footer, search } |
10 | 7 |
|
11 | | - static const String security = '/tweaks/security'; |
12 | | - static const String performance = '/tweaks/performance'; |
13 | | - static const String personalization = '/tweaks/personalization'; |
14 | | - static const String utilities = '/tweaks/utilities'; |
15 | | - static const String updates = '/tweaks/updates'; |
| 8 | +enum RouteMeta { |
| 9 | + home( |
| 10 | + path: '/', |
| 11 | + section: RouteSection.main, |
| 12 | + icon: msicons.FluentIcons.home_24_regular, |
| 13 | + ), |
| 14 | + tweaks( |
| 15 | + path: '/tweaks', |
| 16 | + section: RouteSection.main, |
| 17 | + icon: msicons.FluentIcons.wrench_24_regular, |
| 18 | + ), |
| 19 | + msStore( |
| 20 | + path: '/msstore', |
| 21 | + section: RouteSection.main, |
| 22 | + icon: msicons.FluentIcons.store_microsoft_24_regular, |
| 23 | + ), |
| 24 | + settings( |
| 25 | + path: '/settings', |
| 26 | + section: RouteSection.footer, |
| 27 | + icon: msicons.FluentIcons.settings_24_regular, |
| 28 | + ), |
| 29 | + tweaksSecurity( |
| 30 | + path: '/tweaks/security', |
| 31 | + section: RouteSection.search, |
| 32 | + icon: msicons.FluentIcons.shield_lock_20_regular, |
| 33 | + ), |
| 34 | + tweaksPerformance( |
| 35 | + path: '/tweaks/performance', |
| 36 | + section: RouteSection.search, |
| 37 | + icon: msicons.FluentIcons.top_speed_24_regular, |
| 38 | + ), |
| 39 | + tweaksPersonalization( |
| 40 | + path: '/tweaks/personalization', |
| 41 | + section: RouteSection.search, |
| 42 | + icon: msicons.FluentIcons.color_24_regular, |
| 43 | + ), |
| 44 | + tweaksUtilities( |
| 45 | + path: '/tweaks/utilities', |
| 46 | + section: RouteSection.search, |
| 47 | + icon: msicons.FluentIcons.toolbox_24_regular, |
| 48 | + ), |
| 49 | + tweaksUpdates( |
| 50 | + path: '/tweaks/updates', |
| 51 | + section: RouteSection.search, |
| 52 | + icon: msicons.FluentIcons.arrow_download_24_regular, |
| 53 | + ); |
16 | 54 |
|
17 | | - static const String unsupported = '/unsupported'; |
| 55 | + const RouteMeta({ |
| 56 | + required this.path, |
| 57 | + required this.section, |
| 58 | + required this.icon, |
| 59 | + }); |
18 | 60 |
|
19 | | - static String getRouteName(String path, BuildContext context) { |
20 | | - switch (path) { |
21 | | - case home: |
| 61 | + final String path; |
| 62 | + final RouteSection section; |
| 63 | + final IconData icon; |
| 64 | + |
| 65 | + String get label { |
| 66 | + switch (this) { |
| 67 | + case RouteMeta.home: |
22 | 68 | return t.pageHome; |
23 | | - case tweaks: |
| 69 | + case RouteMeta.tweaks: |
24 | 70 | return t.pageTweaks; |
25 | | - case msStore: |
| 71 | + case RouteMeta.msStore: |
26 | 72 | return t.pageMSStore; |
27 | | - case settings: |
| 73 | + case RouteMeta.settings: |
28 | 74 | return t.pageSettings; |
29 | | - default: |
30 | | - final segment = path.split('/').last; |
31 | | - return segment.isEmpty ? 'Home' : segment.capitalize(); |
| 75 | + case RouteMeta.tweaksSecurity: |
| 76 | + return t.pageTweaksSecurity; |
| 77 | + case RouteMeta.tweaksPerformance: |
| 78 | + return t.pageTweaksPerformance; |
| 79 | + case RouteMeta.tweaksPersonalization: |
| 80 | + return t.pageTweaksPersonalization; |
| 81 | + case RouteMeta.tweaksUtilities: |
| 82 | + return t.pageTweaksUtilities; |
| 83 | + case RouteMeta.tweaksUpdates: |
| 84 | + return t.pageTweaksUpdates; |
32 | 85 | } |
33 | 86 | } |
34 | 87 |
|
| 88 | + static final _pathLookup = {for (final r in values) r.path: r}; |
| 89 | + |
| 90 | + static RouteMeta? fromPath(String path, {bool allowPrefix = false}) { |
| 91 | + if (!allowPrefix) return _pathLookup[path]; |
| 92 | + for (final route in _navigationRoutes) { |
| 93 | + if (path == route.path || |
| 94 | + (route.path != '/' && path.startsWith('${route.path}/'))) { |
| 95 | + return route; |
| 96 | + } |
| 97 | + } |
| 98 | + return null; |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +class AppRoutes { |
| 103 | + static const String unsupported = '/unsupported'; |
| 104 | + |
| 105 | + static const List<RouteMeta> navigationRoutes = _navigationRoutes; |
| 106 | + |
| 107 | + static final mainPaneItems = _buildPaneItems(_mainNavigationRoutes); |
| 108 | + static final footerPaneItems = _buildPaneItems(_footerNavigationRoutes); |
| 109 | + static final searchableItems = _buildPaneItems(_searchableRoutes); |
| 110 | + |
| 111 | + static String getRouteName(String path, BuildContext context) { |
| 112 | + final meta = RouteMeta.fromPath(path); |
| 113 | + if (meta != null) { |
| 114 | + return meta.label; |
| 115 | + } |
| 116 | + final segment = path.split('/').last; |
| 117 | + return segment.isEmpty ? t.pageHome : segment.capitalize(); |
| 118 | + } |
| 119 | + |
| 120 | + static int? getPaneIndexFromRoute(RouteMeta? route) => |
| 121 | + route != null && route.section != RouteSection.search |
| 122 | + ? route.index |
| 123 | + : null; |
| 124 | + |
| 125 | + static final _breadcrumbsCache = <String, List<BreadcrumbItem<String>>>{}; |
| 126 | + |
35 | 127 | static List<BreadcrumbItem<String>> buildBreadcrumbs( |
36 | 128 | String location, |
37 | 129 | BuildContext context, |
38 | 130 | ) { |
39 | | - final segments = location.split('/').where((s) => s.isNotEmpty).toList(); |
40 | | - final breadcrumbs = <BreadcrumbItem<String>>[]; |
41 | | - final theme = FluentTheme.of(context); |
42 | | - |
43 | | - String currentPath = ''; |
44 | | - for (int i = 0; i < segments.length; i++) { |
45 | | - currentPath += '/${segments[i]}'; |
46 | | - final name = getRouteName(currentPath, context); |
47 | | - final isLast = i == segments.length - 1; |
48 | | - |
49 | | - breadcrumbs.add( |
50 | | - BreadcrumbItem( |
51 | | - label: Text( |
52 | | - name, |
53 | | - style: TextStyle( |
54 | | - color: isLast |
55 | | - ? theme.typography.body?.color |
56 | | - : theme.resources.textFillColorSecondary, |
57 | | - ), |
58 | | - ), |
59 | | - value: currentPath, |
60 | | - ), |
61 | | - ); |
62 | | - } |
| 131 | + return _breadcrumbsCache.putIfAbsent(location, () { |
| 132 | + final segments = location.split('/').where((s) => s.isNotEmpty).toList(); |
| 133 | + final theme = FluentTheme.of(context); |
63 | 134 |
|
64 | | - return breadcrumbs; |
| 135 | + String currentPath = ''; |
| 136 | + return [ |
| 137 | + for (int i = 0; i < segments.length; i++) |
| 138 | + (() { |
| 139 | + currentPath += '/${segments[i]}'; |
| 140 | + final isLast = i == segments.length - 1; |
| 141 | + return BreadcrumbItem( |
| 142 | + label: Text( |
| 143 | + getRouteName(currentPath, context), |
| 144 | + style: TextStyle( |
| 145 | + color: isLast |
| 146 | + ? theme.typography.body?.color |
| 147 | + : theme.resources.textFillColorSecondary, |
| 148 | + ), |
| 149 | + ), |
| 150 | + value: currentPath, |
| 151 | + ); |
| 152 | + })(), |
| 153 | + ]; |
| 154 | + }); |
65 | 155 | } |
66 | 156 |
|
67 | 157 | /// Creates a page with [HorizontalSlidePageTransition] for nested routes. |
@@ -92,6 +182,41 @@ class AppRoutes { |
92 | 182 | } |
93 | 183 | } |
94 | 184 |
|
| 185 | +const List<RouteMeta> _mainNavigationRoutes = [ |
| 186 | + RouteMeta.home, |
| 187 | + RouteMeta.tweaks, |
| 188 | + RouteMeta.msStore, |
| 189 | +]; |
| 190 | + |
| 191 | +const List<RouteMeta> _footerNavigationRoutes = [RouteMeta.settings]; |
| 192 | + |
| 193 | +const List<RouteMeta> _searchableRoutes = [ |
| 194 | + RouteMeta.tweaksSecurity, |
| 195 | + RouteMeta.tweaksPerformance, |
| 196 | + RouteMeta.tweaksPersonalization, |
| 197 | + RouteMeta.tweaksUtilities, |
| 198 | + RouteMeta.tweaksUpdates, |
| 199 | +]; |
| 200 | + |
| 201 | +const List<RouteMeta> _navigationRoutes = [ |
| 202 | + ..._mainNavigationRoutes, |
| 203 | + ..._footerNavigationRoutes, |
| 204 | +]; |
| 205 | + |
| 206 | +List<NavigationPaneItem> _buildPaneItems(List<RouteMeta> routes) { |
| 207 | + return routes |
| 208 | + .map( |
| 209 | + (route) => PaneItem( |
| 210 | + key: ValueKey(route.path), |
| 211 | + icon: Icon(route.icon, size: 20), |
| 212 | + title: Text(route.label), |
| 213 | + body: const SizedBox.shrink(), |
| 214 | + ), |
| 215 | + ) |
| 216 | + .toList(growable: false) |
| 217 | + .cast<NavigationPaneItem>(); |
| 218 | +} |
| 219 | + |
95 | 220 | extension _StringExtension on String { |
96 | 221 | String capitalize() { |
97 | 222 | if (isEmpty) return this; |
|
0 commit comments