The Garage SDKs, in the open
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
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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(":");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user