LaunchQueue
How an installed app receives the files, and the target URL, it was launched with - the .NET side of the manifest's file_handlers, protocol_handlers and share_target.
@inject Bit.Butil.LaunchQueue launchQueueMDN reference
OnAfterRenderAsync is safe. Under
UseLazyScripts, call IsSupported() early in start-up so the module is
imported before the launch arrives.
IsSupported is true when the runtime exposes window.launchQueue. SupportsFiles is a separate question: a runtime can deliver a launch without ever putting files on it.
@inject Bit.Butil.LaunchQueue launchQueue
var supported = await launchQueue.IsSupported();
var withFiles = await launchQueue.SupportsFiles();Registers the handler that receives this app's launch, with the target URL and any files. It fires immediately when the launch has already arrived, so registering late is safe. Dispose the returned subscription in DisposeAsync.
"/open"
IAsyncDisposable
Bit.Butil.LaunchQueue launchQueue
{
private string? _targetUrl;
private LaunchFile[] _files = [];
private ButilSubscription? _sub;
// Safe here rather than in OnInitialized: the consumer fires immediately when the launch has
// already arrived, so registering after the first render loses nothing.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
_sub = await launchQueue.SetConsumer(launch =>
{
_targetUrl = launch.TargetUrl;
_files = launch.Files;
InvokeAsync(StateHasChanged);
});
}
public async ValueTask DisposeAsync()
{
if (_sub is not null) await _sub.DisposeAsync();
}
}{
"name": "Bit.Butil demo",
"start_url": "/",
"display": "standalone",
"icons": [{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }],
"//": "Nothing reaches SetConsumer without these. The app must also be installed: a page in a tab is never launched by the OS.",
"file_handlers": [
{ "action": "/open", "accept": { "text/markdown": [".md"] } }
],
"protocol_handlers": [
{ "protocol": "web+butil", "url": "/open?link=%s" }
],
"launch_handler": { "client_mode": "focus-existing" }
}A launched file's handle stays on the JS side - it cannot cross interop - so contents are read by the file's Index. Both overloads take either the LaunchFile or its index; pass the LaunchFile where an app can be launched more than once, since it carries the LaunchId that keeps it pointing at its own launch's files rather than at the newest launch's.
foreach (var file in launch.Files)
{
var text = await launchQueue.ReadText(file);
if (text is not null) Open(file.Name, text);
}The 'save' half of an editor launched through file_handlers. The browser asks for the write permission on the first save; a refusal returns false rather than throwing.
var saved = await launchQueue.WriteText(file, editorContents);
if (saved is false)
{
// the user refused the write permission, or the handle went stale
}None of this fires without the manifest declaring what the app handles. file_handlers registers the app for a file type, protocol_handlers for a URL scheme, and launch_handler decides whether a second launch opens a new window or reuses the existing one.
{
"file_handlers": [
{ "action": "/open", "accept": { "text/markdown": [".md"] } }
],
"protocol_handlers": [
{ "protocol": "web+butil", "url": "/open?link=%s" }
],
"launch_handler": { "client_mode": "focus-existing" }
}API reference
ValueTask<bool> IsSupported()ValueTask<bool> SupportsFiles()Task<ButilSubscription> SetConsumer(Action<LaunchParams> handler)ValueTask<string?> ReadText(LaunchFile file) / ReadText(int index)ValueTask<byte[]?> ReadBytes(LaunchFile file) / ReadBytes(int index)ValueTask<bool> WriteText(LaunchFile file, string contents) / WriteText(int index, string contents)ValueTask<bool> WriteBytes(LaunchFile file, byte[] contents) / WriteBytes(int index, byte[] contents)ValueTask DisposeAsync()