import * as TID from "@atcute/tid"; import decodeQR from "@paulmillr/qr/decode.js"; import { html } from "lit-html"; import * as Output from "~/common/output.js"; import { NAME as S3_NAME } from "~/components/output/bytes/s3/element.js"; import { SCHEME as S3_SCHEME } from "~/components/input/s3/constants.js"; import { bucketId, buildURI, parseURI } from "~/components/input/s3/common.js"; import { effect } from "~/common/signal.js"; import foundation from "~/common/foundation.js"; import { setup, waitForOutputOption } from "~/facets/connect/common.js"; foundation.setup({ title: "Connect S3" }); /** * @import { Bucket } from "@specs/components/input/s3/types.d.ts" * @import { S3OutputElement } from "@specs/components/output/bytes/s3/types.d.ts" */ //////////////////////////////////////////// // SETUP //////////////////////////////////////////// const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = await Promise.all([ foundation.configurator.input(), foundation.orchestrator.output(), foundation.orchestrator.sources(), ]); await Promise.all([ customElements.whenDefined(inputConfigurator.localName), customElements.whenDefined(outputOrchestrator.localName), customElements.whenDefined(sourcesOrchestrator.localName), ]); const s3El = /** @type {S3OutputElement | undefined} */ ( outputOrchestrator.root().querySelector(S3_NAME) ); const s3Option = await waitForOutputOption(outputOrchestrator, "S3"); const OUTPUT_S3_ID = s3Option.id; //////////////////////////////////////////// // CAMERA SCAN //////////////////////////////////////////// /** @type {MediaStream | null} */ let scanStream = null; /** @type {number} */ let scanTimerId = 0; /** @param {boolean} scanning */ function setScanButtonLabel(scanning) { const btn = document.querySelector("#s3-scan-btn"); if (!btn) return; btn.innerHTML = scanning ? ` Cancel scan` : ` Scan QR code`; } function stopScan() { scanStream?.getTracks().forEach((t) => t.stop()); scanStream = null; const scanVideo = document.querySelector("#s3-scan-video"); if (scanVideo instanceof HTMLVideoElement) scanVideo.hidden = true; clearTimeout(scanTimerId); setScanButtonLabel(false); } async function handleScanClick() { if (scanStream) { stopScan(); return; } setScanError(null); const scanVideo = document.querySelector("#s3-scan-video"); if (!(scanVideo instanceof HTMLVideoElement)) return; try { scanStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, }); scanVideo.srcObject = scanStream; scanVideo.hidden = false; setScanButtonLabel(true); const canvas = document.createElement("canvas"); const ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext("2d")); const loop = () => { if (!scanStream) return; if ( scanVideo.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && scanVideo.videoWidth > 0 ) { canvas.width = scanVideo.videoWidth; canvas.height = scanVideo.videoHeight; ctx.drawImage(scanVideo, 0, 0); try { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const result = decodeQR(/** @type {any} */ (imageData)); const parsed = parseURI(result); if (parsed) { const { bucket } = parsed; /** @param {string} id @returns {HTMLInputElement} */ const field = ( id, ) => /** @type {HTMLInputElement} */ (document.querySelector(id)); field("#s3-access-key").value = bucket.accessKey; field("#s3-secret-key").value = bucket.secretKey; field("#s3-bucket-name").value = bucket.bucketName; field("#s3-host").value = bucket.host; field("#s3-region").value = bucket.region; field("#s3-path").value = bucket.path; stopScan(); return; } } catch { /* no QR found in this frame */ } } scanTimerId = window.setTimeout(loop, 200); }; loop(); } catch { setScanError("Could not access camera"); } } // The output configurator can't find custom outputs (added by the // output-bundle prelude) during its initial connectedCallback, so it // re-loads the selected output via `loadSelected()` which is async. // Wait for that to settle before rendering the UI, otherwise // `selected()` is still null here and the "Use as userdata storage" // button would show even though S3 is already configured as the // user's output. if (outputOrchestrator.hasSelected() && !outputOrchestrator.selected()) { await new Promise((resolve) => { const stop = effect(() => { if (outputOrchestrator.selected()) { stop(); resolve(undefined); } }); }); } //////////////////////////////////////////// // UI //////////////////////////////////////////// const { setItems, setError, setDialogError, showQR } = setup({ title: "S3", description: html`
Connect to an S3-compatible storage to use it as audio input or user-data storage.
A custom syncing strategy is used for the user-data storage, tracking what was added and removed so conflicts can be resolved.
`, formFields: html`* Required fields
`, footerActions: html` `, onSubmit: (mode) => addBucket(mode), onOutputActivate: async () => { await outputOrchestrator.select(OUTPUT_S3_ID); }, }); const setScanError = setDialogError; document.querySelector("#connect-dialog")?.addEventListener("close", stopScan); const accessKeyInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-access-key")); const secretKeyInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-secret-key")); const bucketNameInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-bucket-name")); const hostInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-host")); const regionInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-region")); const pathInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-path")); //////////////////////////////////////////// // REACTIVE LIST //////////////////////////////////////////// effect(() => { const inputSources = sourcesOrchestrator.sources()[S3_SCHEME] ?? []; const outputBucket = s3El?.bucket(); const isSelectedOutput = outputOrchestrator.selected()?.id === OUTPUT_S3_ID; /** @type {Map