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:
ImBenji
2026-09-23 18:49:21 +01:00
co-authored by Claude Opus 5.5
commit b269201919
117 changed files with 26944 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import "package:flutter/foundation.dart";
import "package:flutter/services.dart";
// hides the OS cursor and keeps it visually pinned at its start point for a
// drag - see macos/Runner/CursorLock.swift for the actual warp-and-suppress
// implementation. no native handler is registered on platforms other than
// macOS, so [lock]/[unlock] just fail (and get logged) instead of doing
// anything - callers dont need to guard by platform themselves, they just
// wont get live deltas or a frozen cursor there.
//
// Deltas come back over the SAME channel as a native-to-dart call rather
// than through Flutters own PointerMoveEvent - the native side intercepts
// and swallows the underlying mouse-dragged events entirely (see the Swift
// side for why), so Flutters own pointer delta cannot be trusted at all
// while locked.
class CursorLock {
CursorLock._();
static const _channel = MethodChannel("garage/cursor_lock");
// whether theres actually a native side listening. anywhere else the
// lock/unlock calls are no-ops that throw a MissingPluginException, and -
// more importantly - no deltas ever come back, so callers need to know to
// drive themselves off Flutters own pointer stream instead.
static bool get isSupported =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
static void Function(double dx, double dy)? _onDelta;
static bool _handlerInstalled = false;
static void _ensureHandlerInstalled() {
if (_handlerInstalled) return;
_handlerInstalled = true;
_channel.setMethodCallHandler((call) async {
if (call.method != "scrubDelta") return;
final args = call.arguments as Map;
final dx = (args["dx"] as num).toDouble();
final dy = (args["dy"] as num).toDouble();
_onDelta?.call(dx, dy);
});
}
// locks the cursor and starts forwarding raw pointer deltas to [onDelta]
// until [unlock] is called. only one scrub can be active at a time (theres
// only one OS cursor to hide), so this just replaces whatever listener was
// there before.
static void lock(void Function(double dx, double dy) onDelta) {
_ensureHandlerInstalled();
_onDelta = onDelta;
if (!isSupported) return;
_channel.invokeMethod("lock").catchError((e, st) {
debugPrint("CursorLock.lock failed: $e\n$st");
});
}
static void unlock() {
_onDelta = null;
if (!isSupported) return;
_channel.invokeMethod("unlock").catchError((e, st) {
debugPrint("CursorLock.unlock failed: $e\n$st");
});
}
}
+23
View File
@@ -0,0 +1,23 @@
import "package:flutter/widgets.dart";
import "eyedropper_native.dart"
if (dart.library.html) "eyedropper_web.dart"
as impl;
// Screen-wide colour sampling, where the platform can actually do it.
//
// macOS gets NSColorSampler (see macos/Runner/ScreenColorSampler.swift) and
// Chromium browsers get the EyeDropper web api. Both of those draw their own
// magnifier loupe over the whole desktop and hand back a single colour when
// the user clicks, so theres no live preview to stream on the way - the
// preview IS the loupe.
//
// Everywhere else [nativeEyedropperAvailable] just answers false and callers
// fall back to sampling the apps own frame (see widgets/app_frame_capture.dart),
// which is window-only but works on every platform.
Future<bool> nativeEyedropperAvailable() => impl.nativeEyedropperAvailable();
/// Opens the system sampler and resolves with the picked colour, or null if
/// the user cancelled (escape) or something went wrong on the way.
Future<Color?> pickNativeScreenColor() => impl.pickNativeScreenColor();
@@ -0,0 +1,48 @@
import "package:flutter/foundation.dart";
import "package:flutter/services.dart";
import "package:flutter/widgets.dart";
// macOS side of the eyedropper. Everything else that isnt web lands here too
// and simply reports "no native sampler", which is the honest answer - theres
// no handler registered on windows/linux/ios/android.
const _channel = MethodChannel("garage/eyedropper");
bool? _cachedAvailability;
Future<bool> nativeEyedropperAvailable() async {
if (_cachedAvailability != null) return _cachedAvailability!;
if (defaultTargetPlatform != TargetPlatform.macOS) {
_cachedAvailability = false;
return false;
}
try {
_cachedAvailability = await _channel.invokeMethod<bool>("isAvailable");
} catch (error, stack) {
// an older macOS (or a runner thats never been rebuilt with the swift
// side in it) - not fatal, we just fall back to in-app sampling.
debugPrint("eyedropper: isAvailable failed: $error\n$stack");
_cachedAvailability = false;
}
return _cachedAvailability ?? false;
}
Future<Color?> pickNativeScreenColor() async {
try {
final picked = await _channel.invokeMapMethod<String, int>("pick");
if (picked == null) return null;
final r = picked["r"];
final g = picked["g"];
final b = picked["b"];
if (r == null || g == null || b == null) {
debugPrint("eyedropper: native pick returned a malformed colour $picked");
return null;
}
return Color.fromARGB(255, r, g, b);
} catch (error, stack) {
debugPrint("eyedropper: native pick failed: $error\n$stack");
return null;
}
}
@@ -0,0 +1,50 @@
// ignore_for_file: avoid_web_libraries_in_flutter
import "dart:js_interop";
import "dart:js_interop_unsafe";
import "package:flutter/widgets.dart";
// EyeDropper is a chromium-only api at the time of writing (no firefox, no
// safari), hence the feature check rather than just assuming its there on web.
// https://developer.mozilla.org/en-US/docs/Web/API/EyeDropper
@JS("EyeDropper")
extension type _EyeDropper._(JSObject _) implements JSObject {
external _EyeDropper();
external JSPromise<_EyeDropperResult> open();
}
extension type _EyeDropperResult._(JSObject _) implements JSObject {
external String get sRGBHex;
}
Future<bool> nativeEyedropperAvailable() async =>
globalContext.has("EyeDropper");
Future<Color?> pickNativeScreenColor() async {
try {
final result = await _EyeDropper().open().toDart;
return _parseHex(result.sRGBHex);
} catch (error, stack) {
// cancelling with escape rejects the promise with an AbortError, so this
// path is completely normal - still worth a line in the console though.
debugPrint("eyedropper: web pick ended without a colour: $error\n$stack");
return null;
}
}
Color? _parseHex(String value) {
final hex = value.replaceAll("#", "");
if (hex.length != 6) {
debugPrint("eyedropper: unexpected sRGBHex \"$value\"");
return null;
}
final parsed = int.tryParse(hex, radix: 16);
if (parsed == null) {
debugPrint("eyedropper: could not parse sRGBHex \"$value\"");
return null;
}
return Color(0xFF000000 | parsed);
}