garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
236 lines
6.6 KiB
Dart
236 lines
6.6 KiB
Dart
import "package:flutter/widgets.dart";
|
|
|
|
sealed class AppMenuItem {
|
|
const AppMenuItem();
|
|
}
|
|
|
|
class AppMenuGroup extends AppMenuItem {
|
|
const AppMenuGroup({
|
|
required this.label,
|
|
required this.children,
|
|
this.icon,
|
|
this.visible = true,
|
|
});
|
|
|
|
final String label;
|
|
final IconData? icon;
|
|
final List<AppMenuItem> children;
|
|
final bool visible;
|
|
|
|
List<AppMenuItem> get visibleChildren => children.where((item) {
|
|
return switch (item) {
|
|
AppMenuGroup group => group.visible,
|
|
AppMenuAction action => action.visible,
|
|
_ => true,
|
|
};
|
|
}).toList();
|
|
}
|
|
|
|
class AppMenuAction extends AppMenuItem {
|
|
const AppMenuAction({
|
|
required this.label,
|
|
this.icon,
|
|
this.trailingIcon,
|
|
this.onTap,
|
|
this.shortcut,
|
|
this.visible = true,
|
|
});
|
|
|
|
final String label;
|
|
final IconData? icon;
|
|
final IconData? trailingIcon;
|
|
final VoidCallback? onTap;
|
|
final SingleActivator? shortcut;
|
|
final bool visible;
|
|
|
|
bool get enabled => onTap != null;
|
|
}
|
|
|
|
class AppMenuCheck extends AppMenuItem {
|
|
const AppMenuCheck({
|
|
required this.label,
|
|
required this.checked,
|
|
this.checkedLabel,
|
|
this.onToggle,
|
|
this.shortcut,
|
|
});
|
|
|
|
final String label;
|
|
final String? checkedLabel;
|
|
final bool checked;
|
|
final VoidCallback? onToggle;
|
|
final SingleActivator? shortcut;
|
|
}
|
|
|
|
class AppMenuPlatformProvided extends AppMenuItem {
|
|
const AppMenuPlatformProvided(this.type);
|
|
|
|
final PlatformProvidedMenuItemType type;
|
|
}
|
|
|
|
class AppMenuSeparator extends AppMenuItem {
|
|
const AppMenuSeparator({this.visible = true});
|
|
|
|
final bool visible;
|
|
}
|
|
|
|
/// Converts the shared menu model to Flutter's native platform menu model.
|
|
class AppMenuNativeRenderer {
|
|
static List<PlatformMenuItem> build(
|
|
List<AppMenuGroup> menus, {
|
|
String appName = "Garage App",
|
|
}) {
|
|
final appItems = <PlatformMenuItem>[
|
|
for (final type in [
|
|
PlatformProvidedMenuItemType.about,
|
|
PlatformProvidedMenuItemType.servicesSubmenu,
|
|
PlatformProvidedMenuItemType.hide,
|
|
PlatformProvidedMenuItemType.hideOtherApplications,
|
|
PlatformProvidedMenuItemType.showAllApplications,
|
|
PlatformProvidedMenuItemType.quit,
|
|
])
|
|
if (PlatformProvidedMenuItem.hasMenu(type))
|
|
PlatformProvidedMenuItem(type: type),
|
|
];
|
|
|
|
return [
|
|
if (appItems.isNotEmpty) PlatformMenu(label: appName, menus: appItems),
|
|
for (final group in menus)
|
|
if (group.visible)
|
|
PlatformMenu(
|
|
label: group.label,
|
|
menus: _convertItems(group.visibleChildren),
|
|
),
|
|
];
|
|
}
|
|
|
|
static String signature(List<AppMenuGroup> menus) {
|
|
final buffer = StringBuffer();
|
|
for (final group in menus) {
|
|
if (!group.visible) continue;
|
|
_writeSignature(buffer, group);
|
|
buffer.write(";");
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
static List<PlatformMenuItem> _convertItems(List<AppMenuItem> items) {
|
|
final groups = <List<AppMenuItem>>[<AppMenuItem>[]];
|
|
for (final item in items) {
|
|
if (item case AppMenuSeparator(visible: true)) {
|
|
groups.add(<AppMenuItem>[]);
|
|
} else if (item is! AppMenuSeparator) {
|
|
groups.last.add(item);
|
|
}
|
|
}
|
|
|
|
final result = <PlatformMenuItem>[];
|
|
for (var index = 0; index < groups.length; index++) {
|
|
final native = groups[index]
|
|
.map(_toNativeItem)
|
|
.whereType<PlatformMenuItem>();
|
|
final items = native.toList();
|
|
if (items.isEmpty) continue;
|
|
if (index == 0) {
|
|
result.addAll(items);
|
|
} else {
|
|
result.add(PlatformMenuItemGroup(members: items));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static PlatformMenuItem? _toNativeItem(AppMenuItem item) {
|
|
return switch (item) {
|
|
AppMenuGroup group when group.visible => PlatformMenu(
|
|
label: group.label,
|
|
menus: _convertItems(group.visibleChildren),
|
|
),
|
|
AppMenuAction action when action.visible => PlatformMenuItem(
|
|
label: action.label,
|
|
shortcut: action.shortcut,
|
|
onSelected: action.onTap,
|
|
),
|
|
AppMenuCheck check => PlatformMenuItem(
|
|
label: check.checked && check.checkedLabel != null
|
|
? check.checkedLabel!
|
|
: check.label,
|
|
shortcut: check.shortcut,
|
|
onSelected: check.onToggle,
|
|
),
|
|
AppMenuPlatformProvided provided
|
|
when PlatformProvidedMenuItem.hasMenu(provided.type) =>
|
|
PlatformProvidedMenuItem(type: provided.type),
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
static void _writeSignature(StringBuffer buffer, AppMenuItem item) {
|
|
switch (item) {
|
|
case AppMenuGroup group:
|
|
if (!group.visible) return;
|
|
buffer
|
|
..write("G(")
|
|
..write(group.label)
|
|
..write("|")
|
|
..write(group.icon?.codePoint ?? -1)
|
|
..write(")[");
|
|
for (final child in group.visibleChildren) {
|
|
_writeSignature(buffer, child);
|
|
buffer.write(",");
|
|
}
|
|
buffer.write("]");
|
|
case AppMenuAction action:
|
|
if (!action.visible) return;
|
|
buffer
|
|
..write("A(")
|
|
..write(action.label)
|
|
..write("|")
|
|
..write(action.icon?.codePoint ?? -1)
|
|
..write("|")
|
|
..write(action.enabled ? 1 : 0)
|
|
..write("|")
|
|
..write(_shortcutSignature(action.shortcut))
|
|
..write(")");
|
|
case AppMenuCheck check:
|
|
buffer
|
|
..write("C(")
|
|
..write(check.label)
|
|
..write("|")
|
|
..write(check.checkedLabel ?? "-")
|
|
..write("|")
|
|
..write(check.checked ? 1 : 0)
|
|
..write("|")
|
|
..write(check.onToggle != null ? 1 : 0)
|
|
..write("|")
|
|
..write(_shortcutSignature(check.shortcut))
|
|
..write(")");
|
|
case AppMenuPlatformProvided provided:
|
|
buffer
|
|
..write("P(")
|
|
..write(provided.type.name)
|
|
..write(")");
|
|
case AppMenuSeparator separator:
|
|
if (separator.visible) buffer.write("S");
|
|
}
|
|
}
|
|
|
|
// SingleActivator has no value-based toString (it's Diagnosticable, so the
|
|
// default one embeds the object's identity hash) - and menu_def.dart builds
|
|
// a fresh activator on every rebuild, so hashing the instance itself would
|
|
// make the signature change every frame and defeat the whole point of this
|
|
// gate. build it off the actual key/modifier fields instead, those are what
|
|
// we care about being stable.
|
|
static String _shortcutSignature(SingleActivator? shortcut) {
|
|
if (shortcut == null) return "-";
|
|
return [
|
|
shortcut.trigger.keyLabel,
|
|
shortcut.trigger.keyId.toRadixString(16),
|
|
shortcut.alt ? "a" : "",
|
|
shortcut.control ? "c" : "",
|
|
shortcut.meta ? "m" : "",
|
|
shortcut.shift ? "s" : "",
|
|
].join(":");
|
|
}
|
|
}
|