Skip to main content

shiny-appdevicebridge

Generate code using Shiny.AppDeviceBridge, device bridges on your app's own Shiny.Net.HttpServer that also host a web app (Blazor WebAssembly, React, Vue, any static build) inside a .NET MAUI app on Android, iOS, Mac Catalyst, Windows and the maui-labs macOS and Linux heads — served from a loopback HTTP server, updated over the air from a signed release server, and given device access through bridges with typed C# and TypeScript clients

インストールへ移動

ソース情報

リポジトリ
shinyorg/appdevicebridge
ソースの最終更新活動
2026年9月24日 15:16
検出された SKILL.md の言語
英語
スター
0
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
shiny-appdevicebridge
description
Generate code using Shiny.AppDeviceBridge, device bridges on your app's own Shiny.Net.HttpServer that also host a web app (Blazor WebAssembly, React, Vue, any static build) inside a .NET MAUI app on Android, iOS, Mac Catalyst, Windows and the maui-labs macOS and Linux heads — served from a loopback HTTP server, updated over the air from a signed release server, and given device access through bridges with typed C# and TypeScript clients
auto_invoke
true
triggers
["Shiny.AppDeviceBridge","AppDeviceBridge","UseAppDeviceBridge","AddAppDeviceBridge","AppDeviceBridgeBuilder","MauiAppDeviceBridgeBuilder","AddBridge","AppDeviceBridgeOptions","AppDeviceBridgeServer","AppDeviceBridgePolicies","AuthorizeBridges","AllowAnyCallerInDebug","BridgeCallers","IAppDeviceBridgeServerExtension","IWebAppMainThread","IWebAppBackgroundInvoker","Shiny.AppDeviceBridge.WebView","WebAppHost","WebAppHostView","WebAppHostPage","WebAppHostOptions","UseBaseline","ServeWebAppRemotely","ServeWebAppLocally","OnPrepareResponse","ContentTypeOverrides","SelectVariant","client variants","mobile and desktop client","AddWebAppReleases","MapWebAppReleases","IWebAppBridge","[Truncated]"]
# Shiny.AppDeviceBridge You are an expert in Shiny.AppDeviceBridge. Use this skill when the user hosts a web app inside a .NET MAUI app, calls device features from that web app, updates it over the air, or writes a bridge of their own. **Documentation:** https://shinylib.net/appdevicebridge ## The shape of it - `Shiny.AppDeviceBridge` is the **bridge server**, on the app's own Shiny.Net.HttpServer: `http.AddAppDeviceBridge(bridge => …)` on the `ShinyHttpServerBuilder` from `services.AddShinyHttpServer(http => …)`. The server's address, port, TLS, limits, authentication and the app's own endpoints are configured on that same builder; `AppDeviceBridgeOptions` holds only the bridges' concerns (app id, mount points, allowed hosts, the bridge policy), set with `bridge.Configure(o => …)`. `UseAppDeviceBridge` in MAUI calls `AddShinyHttpServer` for you. - **Bridges register on the bridge builder, never on `MauiAppBuilder` or `ShinyHttpServerBuilder`.** `AppDeviceBridgeBuilder` has `Http`, `Services`, `Options`, `Configure(Action<AppDeviceBridgeOptions>)` and `AddBridge<TBridge>()`; creating one registers the bridge server with the built-in host, settings, files and invoke bridges. In MAUI, both `UseAppDeviceBridge` overloads — `(bridge => …, webApp => …)` with a web app, `(bridge => …)` without — hand out a `MauiAppDeviceBridgeBuilder` (adds `Maui`, the `MauiAppBuilder`) and do the host work once: `UseShiny()` (guarded, so an app that already calls it is fine) and the app's dispatcher registered as `IWebAppMainThread`. **Never generate `UseShiny()` for the bridges.** Calling `UseAppDeviceBridge` again adds to the same server. - **Two kinds of bridge package.** - **No MAUI** — BluetoothLE, Obd, Discovery, Wifi, HttpTransfers, Jobs (plain `net10.0`), Locations (GPS/geofences/motion), Notifications, Push, Wearables, Speech, ScreenRecorder, Calendar, Contacts, Health, RpiCamera, Tunnel. They reference only `Shiny.AppDeviceBridge`; their extensions are generic (`TBuilder AddGpsBridge<TBuilder>(this TBuilder bridge) where TBuilder : AppDeviceBridgeBuilder`) and return the builder they were given, so they chain on either builder and run headless (on macOS they register Shiny's core services themselves). - **MAUI** — AppSupport, AppSupport.Linux, AppLinks, Camera, Photos, Folders, Desktop (tray, quick entry). They reference `Shiny.AppDeviceBridge.Maui`, extend `MauiAppDeviceBridgeBuilder`, and do their own MAUI registration through `bridge.Maui` (camera control, tray icon, controls, Essentials, lifecycle events). The app calls none of that. - `AllowWebPermissions` and `UseTrafficMonitor` stay on `MauiAppBuilder`; `AddTrafficRecorder` stays on `ShinyHttpServerBuilder`. - Every bridge route requires `AppDeviceBridgePolicies.Bridges`, which the bridges enforce themselves. - **There is no private server.** Never generate `o.Server`, `o.ConfigureServer`, `o.AddAuthentication` or `o.AddAuthorization` on `AppDeviceBridgeOptions` — they do not exist. Use the builder. - `Shiny.AppDeviceBridge.WebView` adds the **web app host** — the `UseAppDeviceBridge(bridge => …, webApp => …)` overload (bridge delegate first, both required; `http.AddAppDeviceBridge(bridge => …, webApp => …)` without MAUI). One delegate registers the bridges alone; a second configures the web app. It serves the web app straight from a zip (the baseline or a signed download), shown in `WebAppHostView` / `WebAppHostPage`. The WebView trades a one-time launch token for an HttpOnly cookie, which the host adds to the bridge policy. Only the entry document gets `Cache-Control: no-cache`; the host sets no cache header on any other file. `WebAppHostOptions.OnPrepareResponse` runs after that for every file (an app's own cache policy for LAN/tunnel callers), and `ContentTypeOverrides` maps extensions (with the dot) to content types. - Who gets the pages: the WebView (launch session) always; other machines and tunnels only with `ServeWebAppRemotely = true`; a browser on the same device only with `ServeWebAppLocally = true`. Neither option opens the bridges — that is `AuthorizeBridges`. Never generate a custom middleware to let a local browser in. - **Two clients (phone and desktop) at the same URLs**: `webApp.Variants("mobile", "desktop")` plus `webApp.SelectVariant = ctx => …` (cookie, `Sec-CH-UA-Mobile`, `User-Agent`), with one zip holding `mobile/` and `desktop/`. Never generate a second mount point, a redirect to `/desktop`, or two web app hosts for this. The first variant is the default; a null/unknown/throwing selector falls back to it. - The server works without the WebView: bridges only, for callers the policy admits. - **Moving the server at runtime** (a LAN switch, a port setting) is `await http.StopAsync(); http.Options.Address = …; http.Options.Port = …; await http.StartAsync();` on the app's `HttpServer`. `Origin`, the WebView session and an open tunnel follow on their own; never re-register the bridges or rebuild the host to change the port. - **Bridges** are HTTP endpoints under `/_bridge/{name}` (the prefix is configurable) plus one Server-Sent Events stream. One package per bridge, one extension method each on the bridge builder. Host, settings, files and invoke are built in; AppSupport (`app/…`, `/_bridge/sensors`) is its own package, added with `AddAppSupportBridge()`. - **The event stream carries only named topics.** `GET /_bridge/events?topics=a,b` opens it; the first event, `bridge.stream`, carries the id for `PUT /_bridge/events/{id}` `{ "topics": [...] }`. The host runs each topic's native source only while a stream names it, and unhooks it when the page drops it, disconnects, or the source throws (`bridge.error { event, message }`). The typed clients manage topics — never hand-write `new EventSource(...)` in a page that has them. - **Subscribe before starting a session.** BLE scans and characteristic notifications, Health listeners, the OBD monitor, Discovery browses and dictation answer `409 not_listening` without a listener for their result event, and stop by themselves once it has none; each sensor stops once its event has none. `await` the subscription (it resolves once the host is delivering), then start. - **Every bridge has a typed client.** Never generate `fetch("/_bridge/…")` or JSON-object bodies in page code; use the bridge's client — C# for Blazor, TypeScript for everything else. ## The app (MAUI) ```csharp builder .UseMauiApp<App>() .UseAppDeviceBridge( bridge => bridge .Configure(o => o.AppId = "field-app") // AppId, BasePath, AllowedHosts, AuthorizeBridges .AddAppSupportBridge() .AddLocationBridges() .AddCalendarBridge() .AddPhotosBridge() .AddFoldersBridge(), webApp => { webApp.UseBaseline(typeof(App).Assembly, "webapp.zip"); // offline, no update server needed // webApp.UpdateServer = new Uri("https://api.example.com/webapps"); // webApp.PublicKey = "-----BEGIN PUBLIC KEY-----…"; } ); // No web app, bridges only: the other overload, with just the bridge delegate. // builder.UseAppDeviceBridge(bridge => bridge.Configure(o => o.AppId = "kiosk").AddGpsBridge()); // A head adding its own bridges (desktop only, say) adds to the same server: // builder.UseAppDeviceBridge(bridge => bridge.AddTrayIconBridge()); // The server itself — address, auth, the app's own endpoints — on its builder, in any order: builder.Services.AddShinyHttpServer(http => { http.Options.Address = IPAddress.Any; http.AddAuthentication().AddApiKey(k => k.AddKey(key, "kiosk")); http.Configure(server => { server.UseAuthentication(); server.UseAuthorization(); server.MapGet("/api/orders", ctx => …).RequireAuthorization(); }); }, autoStart: false); // MAUI runs no hosted services; UseAppDeviceBridge starts the server public class App : Application { protected override Window CreateWindow(IActivationState? state) => new(new WebAppHostPage()); } ``` - `UseAppDeviceBridge` listens on loopback port 5780 unless the app sets a port of its own. - Without MAUI: `services.AddShinyHttpServer(http => http.AddAppDeviceBridge(bridge => bridge.Configure(o => …).AddRpiCameraBridge().AddTunnel().AddBridge<ClipboardBridge>()))`. A non-MAUI web host is the same overload on the server's builder: `http.AddAppDeviceBridge(bridge => …, webApp => …)`. - Bridge extensions register the Shiny service behind them. Do **not** also call `AddGps()`, `AddBluetoothLE()` and so on. - A platform without an implementation answers `501`; `IHostBridge.GetInfoAsync()` lists every bridge with `IsSupported`. - Platform setup is the underlying library's: usage descriptions, manifest permissions, entitlements. Loopback needs cleartext to `127.0.0.1` on Android and `NSAllowsLocalNetworking` on Apple platforms. ## Bridge packages | Package | Registration (on the bridge builder) | Client package / interface | | --- | --- | --- | | built in | (always) | `Shiny.AppDeviceBridge.Client`: `IHostBridge`, `ISettingsBridge`, `IFilesBridge`, `ILinksBridge` | | `.AppSupport` | `AddAppSupportBridge()` | `IAppBridge` — info, orientation, browser, maps, store, launch at login, share, haptics, connectivity, battery, screen, clipboard; `ISensorsBridge` — start a sensor with a speed and `MinIntervalMs`, readings only as events (`OnCompassAsync`, …), each stopped once nothing listens to its event | | `.AppSupport.Linux` | `AddAppSupportLinux()` on the GTK4 head, after `AddAppSupportBridge()` | battery and energy saver from UPower and power-profiles-daemon, with change events. The maui-labs GTK4 battery never raises them, so a Linux head without this gets no `app.battery` events | | `.Locations` | `AddGpsBridge()`, `AddGeofenceBridge()`, `AddMotionActivityBridge()` | `IGpsBridge`, `IGeofencesBridge`, `IMotionBridge` | | `.BluetoothLE` | `AddBluetoothLEBridge()` | `IBluetoothLEBridge` | | `.Obd` | `AddObdBridge()` | `IObdBridge` | | `.Wifi` | `AddWifiBridge(hotspot)` | `IWifiBridge` | | `.Discovery` | `AddDiscoveryBridge(protocols)` | `IDiscoveryBridge` | | `.Push` | `AddPushBridge()` | `IPushBridge` | | `.Wearables` | `AddWearablesBridge(o => o.Folder = "watch")` — `WearablesBridgeOptions`: `Root` (`data`), `Folder` (`wearables`), `RegisterWearableService` (on) | `IWearablesBridge` — the companion Apple Watch / Wear OS app via Shiny.Wearables 5.8; iOS and Android only, `501` elsewhere | | `.Maps` | `AddMapsBridge(o => { o.OnlineTiles; o.Catalog; o.CatalogPublicKey; o.Directions.OnlineRouteUrl; o.Directions.ApiKey; })` — callable repeatedly, one options instance; `.Maps.Valhalla`: `AddOnDeviceDirections()` | `IMapsBridge`, `IDirectionsBridge` (`Shiny.AppDeviceBridge.Maps.Client`, `AddMapsBridgeClient()`/`AddDirectionsBridgeClient()`, or `AddBridgeMaps()` from `.Maps.Blazor`) — see Maps below | | `.Notifications` | `AddNotificationsBridge()`; a custom delegate: `AddNotificationsBridge(o => o.UseDelegate<MyNotificationDelegate>())` (subclass `WebAppNotificationDelegate`) | `INotificationsBridge` | | `.HttpTransfers` | `AddHttpTransfersBridge()` | `ITransfersBridge` | | `.AppLinks` | `AddAppLinksBridge(o => …)` | `ILinksBridge` (built in) | | `.Health` | `AddHealthBridge()` | `IHealthBridge` | | `.Speech` | `AddSpeechBridge()` | `ISpeechBridge` | | `.ScreenRecorder` | `AddScreenRecorderBridge(o => o.MaxDuration = TimeSpan.FromMinutes(10))` — `ScreenRecorderBridgeOptions`: `Root` (`data`), `Folder` (`screen-recordings`), `MaxDuration` (1 h; `null` for none), `ConfirmStart`, `RegisterScreenRecorder` (on) | `IScreenRecorderBridge` — the device's screen to a video via Shiny.ScreenRecorder; all platforms — see Screen recorder below | | `.Contacts` | `AddContactsBridge()` | `IContactsBridge` | | `.Calendar` | `AddCalendarBridge()` | `ICalendarBridge` | | `.Photos` | `AddPhotosBridge()` | `IPhotosBridge` | | `.Camera` | `AddCameraBridge(o => …)` | `ICameraBridge` — this device's camera driven from a page anywhere; viewfinder MJPEG at `camera/preview` for an `<img>` (not Linux) | | `.Folders` | `AddFoldersBridge()` — also registers `FolderRoots` for folders the app adds by path | `IFoldersBridge` | | `.Desktop` | `AddTrayIconBridge()`, `AddQuickEntryBridge(o => o.HotKey = "Ctrl+Alt+Space")` | `Shiny.AppDeviceBridge.Desktop.Client`: `ITrayBridge`, `IQuickEntryBridge` (desktop only; `501` on mobile) | | `.RpiCamera` | `AddRpiCameraBridge(o => …)` (either builder — a headless Pi's too) | `IRpiCameraBridge` — snapshots, captures into a file root, controls; live MJPEG at `rpicamera/stream` for an `<img>`; `ICameraService.StreamToAsync(stream)` for a pipe with no HTTP, read with `ReadFramesAsync` (Linux + native shim only) | | `.Jobs` | `AddWebAppJob(name, configure)` | native call `job:{name}` with `JobRun` | | `.Tunnel` | `AddTunnel(o => o.Host = QuickTunnelHost.Pinggy)` (either builder) | none — the app opens and closes it (`AppDeviceBridgeTunnel.StartAsync(token)` / `StopAsync()`) from its own UI or endpoints | Client packages are `Shiny.AppDeviceBridge.{Bridge}.Client`, registered with `Add{Name}BridgeClient()` — the name from the interface: `IAppBridge` → `AddAppBridgeClient()`, `ITransfersBridge` → `AddTransfersBridgeClient()`, `ITrayBridge` → `AddTrayBridgeClient()`, `IQuickEntryBridge` → `AddQuickEntryBridgeClient()`. The desktop bridges share `Shiny.AppDeviceBridge.Desktop.Client`. ## Maps and directions `AddMapsBridge(o => …)` adds `/_bridge/maps` and `/_bridge/directions` on every platform. Online by default; offline where the user downloaded a region. - **Tiles:** `GET /_bridge/maps` returns `TilesUrl`/`GlyphsUrl`/`SpritesUrl` templates for MapLibre (never hard-code them). A tile comes from an installed region, then the tile cache (`TileCacheBytes`), then `OnlineTiles` — a `.pmtiles` URL read by Range, or a `{z}/{x}/{y}` template — else `204`. Keys in `OnlineTiles`/`ConfigureRequest` never reach the page. - **Blazor:** reference `Shiny.AppDeviceBridge.Maps.Blazor`, `services.AddWebAppHostClient().AddBridgeMaps()`, and use `<BridgeMap @ref="map" Latitude=… Longitude=… Zoom=… Style="height: 60vh" OnClick=… OnDrawn=… />`. Methods: `AddPinAsync(new MapPin(id, new GeoPoint(lat, lon), label, Draggable: true))`, `AddShapeAsync(new MapShape(id, MapShapeKind.Line|Polygon, points, color))`, `AddCircleAsync`, `ShowRouteAsync(route)`, `SetDrawModeAsync(MapDrawMode.Pin|Line|Polygon|None)`,
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る