import * as TID from "@atcute/tid"; import { html } from "lit-html"; import * as Output from "~/common/output.js"; import { SCHEME } from "~/components/input/dropbox/constants.js"; import { buildURI, parseURI, } from "~/components/input/dropbox/common.js"; import { NAME as DROPBOX_OUTPUT_NAME } from "~/components/output/bytes/dropbox/element.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 Dropbox" }); /** * @import { DropboxOutputElement } from "@specs/components/output/bytes/dropbox/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 dropboxOption = await waitForOutputOption(outputOrchestrator, "Dropbox"); const OUTPUT_DROPBOX_ID = dropboxOption.id; /** Look up the Dropbox output element, which may not exist yet. */ function dropboxOutputEl() { return /** @type {DropboxOutputElement | undefined} */ ( outputOrchestrator.root().querySelector(DROPBOX_OUTPUT_NAME) ); } //////////////////////////////////////////// // OAUTH TOKEN FROM HASH //////////////////////////////////////////// const hashParams = new URLSearchParams(location.hash.slice(1)); let currentToken = hashParams.get("refresh_token"); const outputToken = hashParams.get("output_refresh_token"); const authError = hashParams.get("error"); if (currentToken || outputToken || authError) { history.replaceState({}, "", location.pathname + location.search); } //////////////////////////////////////////// // UI //////////////////////////////////////////// const { setItems, setError } = setup({ title: "Dropbox", hasInput: false, hasOutput: false, description: html`

Add your Dropbox as an audio source, or use it as user-data storage. Authorize with Dropbox to get started.

When used as user-data storage, a custom syncing strategy tracks what was added and removed so conflicts can be resolved.

`, rightContent: html`
`, formFields: html` `, onSubmit: async () => {}, }); const authSection = /** @type {HTMLElement} */ (document.querySelector("#dropbox-auth-section")); const outputBtn = /** @type {HTMLElement} */ (document.querySelector("#dropbox-output-btn")); const addSection = /** @type {HTMLElement} */ (document.querySelector("#dropbox-add-section")); const dirInput = /** @type {HTMLInputElement} */ (document.querySelector("#dropbox-dir")); if (currentToken) { authSection.hidden = true; addSection.hidden = false; } if (authError) { setError("Dropbox authorization failed. Please try again."); } //////////////////////////////////////////// // REACTIVE LIST //////////////////////////////////////////// effect(() => { const inputSources = sourcesOrchestrator.sources()[SCHEME] ?? []; // Re-query the element each run so we pick it up when it's appended. const el = dropboxOutputEl(); const outputToken = el?.refreshToken(); const isSelectedOutput = outputOrchestrator.selected()?.id === OUTPUT_DROPBOX_ID; // Hide the "Use as userdata storage" button when the output is // already selected. When authorized but not selected, the button // stays visible so the user can re-select. if (outputBtn) outputBtn.hidden = isSelectedOutput; /** @type {{ name: string; detail: string; isInput: boolean; isOutput: boolean; isSelectedOutput: boolean; isDisabled?: boolean; onRemove: () => void; onToggleDisabled?: () => void }[]} */ const items = []; for (const source of inputSources) { const parsed = parseURI(source.uri); if (!parsed) continue; items.push({ name: source.label, detail: "Dropbox", isInput: true, isOutput: false, isSelectedOutput: false, isDisabled: sourcesOrchestrator.isDisabled(source.uri), onRemove: () => removeSource(source.uri), onToggleDisabled: () => sourcesOrchestrator.toggle(source.uri), }); } if (outputToken) { items.push({ name: "Dropbox", detail: "App folder", isInput: false, isOutput: true, isSelectedOutput, onRemove: () => removeOutput(), }); } setItems(items); }); //////////////////////////////////////////// // ACTIONS //////////////////////////////////////////// document.querySelector("#dropbox-auth-btn")?.addEventListener("click", () => { const dropboxInput = /** @type {import("~/components/input/dropbox/element.js").default} */ (inputConfigurator.inputs()[SCHEME]); dropboxInput.authorize(); }); document.querySelector("#dropbox-output-btn")?.addEventListener( "click", async () => { const el = dropboxOutputEl(); const existingToken = el?.refreshToken(); if (existingToken) { // Already authorized — just select the output. await outputOrchestrator.select(OUTPUT_DROPBOX_ID); } else { // Not yet authorized — start the OAuth flow. el?.authorize(); } }, ); document.querySelector("#dropbox-add-btn")?.addEventListener( "click", async () => { if (!currentToken) return; setError(null); try { const rawDir = dirInput?.value?.trim() || "/"; const directoryPath = rawDir.startsWith("/") ? rawDir : "/" + rawDir; const account = { refreshToken: currentToken, directoryPath }; const uri = buildURI(account); const now = new Date().toISOString(); const tracksCol = outputOrchestrator.tracks.collection(); const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; await outputOrchestrator.tracks.save([ ...existingTracks, { $type: "sh.diffuse.output.track", id: TID.now(), createdAt: now, updatedAt: now, kind: "placeholder", uri, }, ]); // Reset UI after adding if (dirInput) dirInput.value = ""; currentToken = null; authSection.hidden = false; addSection.hidden = true; } catch (err) { setError( err instanceof Error ? err.message : "Failed to add Dropbox source", ); } }, ); /** @param {string} uri */ async function removeSource(uri) { setError(null); try { const tracks = await Output.data(outputOrchestrator.tracks); const detachedTracks = await inputConfigurator.detach({ fileUriOrScheme: uri, tracks, }); if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); } catch (err) { setError(err instanceof Error ? err.message : "Failed to remove source"); } } async function removeOutput() { setError(null); try { await outputOrchestrator.deselect(); await dropboxOutputEl()?.unsetRefreshToken(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to disconnect Dropbox"); } } //////////////////////////////////////////// // HANDLE OUTPUT OAUTH RETURN //////////////////////////////////////////// // When returning from the Dropbox output OAuth flow, store the token and // select the output. if (outputToken) { setError(null); try { await dropboxOutputEl()?.setRefreshToken(outputToken); await outputOrchestrator.select(OUTPUT_DROPBOX_ID); } catch (err) { setError(err instanceof Error ? err.message : "Failed to connect Dropbox"); } } //////////////////////////////////////////// // 🚀 //////////////////////////////////////////// foundation.ready();