diff --git a/deno.jsonc b/deno.jsonc index 422609f9..f17d0b75 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -38,6 +38,7 @@ "@std/semver": "jsr:@std/semver@^1.0.8", "@std/xml": "jsr:@std/xml@^0.1.0", "@vicary/debounce-microtask": "jsr:@vicary/debounce-microtask@^0.1.8", + "@paulmillr/qr": "jsr:@paulmillr/qr@^0.6.0", "@zip-js/zip-js": "jsr:@zip-js/zip-js@^2", "alien-signals": "npm:alien-signals@^3.1.2", "bs58check": "npm:bs58check@^4.0.0", diff --git a/src/_includes/layouts/diffuse.vto b/src/_includes/layouts/diffuse.vto index 600918f4..33e55d82 100644 --- a/src/_includes/layouts/diffuse.vto +++ b/src/_includes/layouts/diffuse.vto @@ -45,6 +45,9 @@ base: "./" "@awesome.me/webawesome/dist-cdn/": "./vendor/@awesome.me/webawesome/", "@phosphor-icons/web/": "./vendor/@phosphor-icons/web/", + "@paulmillr/qr": "./vendor/@paulmillr/qr/index.js", + "@paulmillr/qr/": "./vendor/@paulmillr/qr/", + "@atcute/cbor": "./vendor/@atcute/cbor/index.js", "@atcute/oauth-browser-client": "./vendor/@atcute/oauth-browser-client/index.js", "@atcute/tid": "./vendor/@atcute/tid/index.js", diff --git a/src/facets/connect/common.css b/src/facets/connect/common.css index e5af6b7b..aea19935 100644 --- a/src/facets/connect/common.css +++ b/src/facets/connect/common.css @@ -49,6 +49,30 @@ opacity: 0.2; } +.s3-scan-video { + border-radius: 0; + inset: 0; + object-fit: cover; + position: absolute; + height: 100%; + width: 100%; + z-index: 1; +} + +.connect-qr-body { + align-items: center; + display: flex; + justify-content: center; + + & svg { + background: white; + border-radius: var(--radius-sm); + height: 240px; + padding: var(--space-sm); + width: 240px; + } +} + .dropzone { align-items: center; border: 2px dashed var(--border-color); diff --git a/src/facets/connect/common.js b/src/facets/connect/common.js index 3330f1c1..93bbac5e 100644 --- a/src/facets/connect/common.js +++ b/src/facets/connect/common.js @@ -1,3 +1,4 @@ +import { encodeQR } from "@paulmillr/qr"; import { html, nothing, render as litRender } from "lit-html"; /** @@ -5,7 +6,7 @@ import { html, nothing, render as litRender } from "lit-html"; */ /** - * @typedef {{ name: string; detail: string; isInput: boolean; isOutput: boolean; isSelectedOutput: boolean; isDisabled?: boolean; onRemove: () => void; onToggleDisabled?: () => void }} ConnectItem + * @typedef {{ name: string; detail: string; isInput: boolean; isOutput: boolean; isSelectedOutput: boolean; isDisabled?: boolean; onShowQR?: () => void; onRemove: () => void; onToggleDisabled?: () => void }} ConnectItem */ /** @@ -21,9 +22,10 @@ import { html, nothing, render as litRender } from "lit-html"; * @param {(mode: 'input' | 'output') => Promise} config.onSubmit * @param {boolean} [config.hasInput] - Whether to show the "Add audio input" button (default: true) * @param {boolean} [config.hasOutput] - Whether to show the "Use as userdata storage" button (default: true) + * @param {TemplateResult | typeof nothing} [config.footerActions] - Extra buttons rendered in the dialog footer * @param {() => Promise} [config.onOutputActivate] - Called instead of opening the dialog when output is already configured but inactive * - * @returns {{ setItems: (items: ConnectItem[]) => void, setError: (message: string | null) => void }} + * @returns {{ setItems: (items: ConnectItem[]) => void, setError: (message: string | null) => void, setDialogError: (message: string | null) => void, showQR: (data: string) => void }} */ export function setup( { @@ -31,6 +33,7 @@ export function setup( description, rightContent = nothing, formFields, + footerActions = nothing, onSubmit, hasInput = true, hasOutput = true, @@ -87,12 +90,23 @@ export function setup(
+ ${formFields} -
+ + + +
+ Scan to connect +
+
+
`, @@ -165,6 +179,21 @@ export function setup( dialog.close(); }); + const qrDialog = + /** @type {HTMLDialogElement} */ (main.querySelector("#connect-qr-dialog")); + const qrBody = + /** @type {HTMLElement} */ (main.querySelector("#connect-qr-body")); + + main.querySelector("#connect-qr-close-btn")?.addEventListener("click", () => { + qrDialog.close(); + }); + + /** @param {string} data */ + const showQR = (data) => { + qrBody.innerHTML = encodeQR(data, "svg"); + qrDialog.showModal(); + }; + const submitBtn = /** @type {HTMLElement} */ (main.querySelector("#connect-submit-btn")); @@ -188,6 +217,8 @@ export function setup( return { setError, + setDialogError, + showQR, /** * Updates the list of configured items below the divider. @@ -203,7 +234,7 @@ export function setup( litRender( html` ${items.map( - ({ name, detail, isInput, isOutput, isSelectedOutput, isDisabled, onRemove, onToggleDisabled }, index) => + ({ name, detail, isInput, isOutput, isSelectedOutput, isDisabled, onShowQR, onRemove, onToggleDisabled }, index) => html`
  • @@ -226,6 +257,19 @@ export function setup(