A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: apply new design to all connect and scrobble facets

+641 -575
+6 -4
src/facets/connect/atproto/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - @import "./facets/connect/common.css" layer(connect); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./facets/connect/common.css"; 4 6 5 - @layer base, diffuse, wa; 7 + @layer base, diffuse; 6 8 </style> 7 9 8 - <main class="wa-theme-default"></main> 10 + <main></main> 9 11 10 12 <script type="module" src="facets/connect/atproto/index.inline.js"></script>
+46 -57
src/facets/connect/atproto/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/callout/callout.js"; 2 - import "@awesome.me/webawesome/dist/components/input/input.js"; 3 - 4 1 import { html, nothing, render as litRender } from "lit-html"; 5 2 6 3 import { NAME as ATPROTO_NAME } from "~/components/output/raw/atproto/element.js"; ··· 13 10 foundation.setup({ title: "Connect AT Protocol | Diffuse" }); 14 11 15 12 /** 16 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 17 13 * @import { ATProtoOutputElement } from "~/components/output/raw/atproto/types.d.ts" 18 14 * @import TrackUriPasskeyTransformer from "~/components/transformer/output/refiner/track-uri-passkey/element.js" 19 15 */ ··· 23 19 //////////////////////////////////////////// 24 20 25 21 const outputOrchestrator = await foundation.orchestrator.output(); 22 + 26 23 await customElements.whenDefined(outputOrchestrator.localName); 24 + await customElements.whenDefined(ATPROTO_NAME); 27 25 28 - const atprotoOption = (await outputOrchestrator.options()).find( 29 - (o) => o.label === "AT Protocol", 30 - ); 26 + // The AT Protocol option is added dynamically by the output-bundle facet, which 27 + // runs in a separate script and appends the element to the output configurator 28 + // via a signal effect. That effect may not have fired yet when this facet loads, 29 + // so we observe the configurator for child-list mutations and resolve as soon as 30 + // the option appears. 31 + const outputConfigurator = outputOrchestrator.outputConfigurator; 31 32 32 - const atprotoEl = /** @type {ATProtoOutputElement | undefined} */ ( 33 - outputOrchestrator.root().querySelector(ATPROTO_NAME) 34 - ); 33 + const atprotoOption = await new Promise((resolve) => { 34 + const check = async () => { 35 + const opt = (await outputOrchestrator.options()).find( 36 + (o) => o.label === "AT Protocol", 37 + ); 38 + if (opt) { 39 + observer.disconnect(); 40 + resolve(opt); 41 + } 42 + }; 35 43 36 - if (!atprotoOption) { 37 - throw new Error("AT Protocol output was not enabled!"); 38 - } 44 + const observer = new MutationObserver(check); 45 + observer.observe(outputConfigurator, { childList: true }); 46 + check(); 47 + }); 39 48 40 49 const ATPROTO_OUTPUT_ID = atprotoOption.id; 50 + 51 + const atprotoEl = /** @type {ATProtoOutputElement | undefined} */ ( 52 + outputOrchestrator.root().querySelector(ATPROTO_NAME) 53 + ); 41 54 42 55 const atprotoPasskeyEl = /** @type {TrackUriPasskeyTransformer | null} */ ( 43 56 outputOrchestrator.root().querySelector( ··· 64 77 <p> 65 78 Connect to your AT Protocol identity to use it as user-data storage. 66 79 </p> 67 - <p class="wa-caption-xs"> 80 + <p class="caption"> 68 81 Your data is stored as lexicon records in your personal data server (PDS). 69 82 </p> 70 83 `, 71 84 72 85 formFields: html` 73 - <wa-input 74 - id="atproto-handle" 75 - label="Handle" 76 - placeholder="you.bsky.social" 77 - required 78 - ></wa-input> 79 - <p class="wa-caption-xs">* Required fields</p> 86 + <label>Handle <input id="atproto-handle" placeholder="you.bsky.social" required></label> 80 87 `, 81 88 82 89 onSubmit: (_mode) => connect(), ··· 87 94 }); 88 95 89 96 const handleInput = 90 - /** @type {WaInput} */ (document.querySelector("#atproto-handle")); 97 + /** @type {HTMLInputElement} */ (document.querySelector("#atproto-handle")); 91 98 92 99 //////////////////////////////////////////// 93 100 // REACTIVE LIST ··· 120 127 121 128 if (atprotoPasskeyEl) { 122 129 const passkeyRoot = document.createElement("div"); 123 - passkeyRoot.classList.add("wa-stack"); 124 - document.querySelector("main .card-body")?.appendChild(passkeyRoot); 130 + document.querySelector("main .facet__right")?.appendChild(passkeyRoot); 125 131 126 132 effect(() => { 127 133 const passkeyActive = atprotoPasskeyEl.passkeyActive() ?? false; ··· 131 137 132 138 litRender( 133 139 html` 134 - <wa-divider style="margin: var(--spacing) 0"></wa-divider> 140 + <hr> 135 141 136 142 <div> 137 143 <strong>Passkey encryption (optional)</strong> ··· 142 148 <p>Passkey active — Track URIs are encrypted.</p> 143 149 144 150 ${passkeyError 145 - ? html` 146 - <wa-callout variant="danger">${passkeyError}</wa-callout> 147 - ` 151 + ? html`<div class="callout callout--danger">${passkeyError}</div>` 148 152 : nothing} 149 153 150 154 <div class="button-row"> 151 - <wa-button 152 - variant="neutral" 153 - appearance="outlined" 154 - @click="${handlePasskeyRemove}" 155 - >Remove passkey</wa-button> 155 + <button @click="${handlePasskeyRemove}">Remove passkey</button> 156 156 </div> 157 157 158 - <p class="wa-caption-xs"> 158 + <p class="caption"> 159 159 Removing the passkey will expose all the sensitive information that was 160 160 previously encrypted. 161 161 </p> 162 162 ` 163 163 : html` 164 - <p class="wa-caption-xs"> 164 + <p class="caption"> 165 165 Track URIs can optionally be encrypted so that passwords and other sensitive 166 166 authentication details are kept private. Note that, with this enabled, other 167 167 people cannot play audio listed on your account. 168 168 </p> 169 169 170 170 ${passkeyError 171 - ? html` 172 - <wa-callout variant="danger">${passkeyError}</wa-callout> 173 - ` 171 + ? html`<div class="callout callout--danger">${passkeyError}</div>` 174 172 : nothing} 175 173 176 174 <div class="button-row"> 177 - <wa-button 178 - variant="neutral" 179 - appearance="outlined" 180 - ?disabled="${passkeyWorking}" 181 - @click="${handlePasskeySetup}" 182 - >${passkeyWorking 183 - ? "Setting up …" 184 - : "Set up passkey encryption"}</wa-button> 185 - <wa-button 186 - variant="neutral" 187 - appearance="outlined" 188 - ?disabled="${passkeyWorking}" 189 - @click="${handlePasskeyAdopt}" 190 - >${passkeyWorking 191 - ? "Authenticating …" 192 - : "Use existing passkey"}</wa-button> 175 + <button ?disabled="${passkeyWorking}" @click="${handlePasskeySetup}"> 176 + ${passkeyWorking ? "Setting up …" : "Set up passkey encryption"} 177 + </button> 178 + <button ?disabled="${passkeyWorking}" @click="${handlePasskeyAdopt}"> 179 + ${passkeyWorking ? "Authenticating …" : "Use existing passkey"} 180 + </button> 193 181 </div> 194 - `} ${lockedTracksCount > 0 182 + `} 183 + ${lockedTracksCount > 0 195 184 ? html` 196 - <wa-callout variant="warning"> 185 + <div class="callout callout--warning"> 197 186 ${lockedTracksCount} encrypted track(s) cannot be played until you unlock them with 198 187 your passkey. 199 - </wa-callout> 188 + </div> 200 189 ` 201 190 : nothing} 202 191 `,
+23 -69
src/facets/connect/common.css
··· 1 - main { 2 - display: flex; 3 - align-items: center; 4 - justify-content: center; 5 - min-height: 100dvh; 6 - padding: var(--wa-space-m); 7 - } 8 - 9 - wa-card { 10 - width: min(400px, calc(100vw - 2rem)); 11 - } 12 - 13 - .card-header { 14 - display: flex; 15 - align-items: center; 16 - justify-content: space-between; 17 - } 18 - 19 - .card-body { 20 - display: flex; 21 - flex-direction: column; 22 - gap: var(--wa-space-m); 23 - } 24 - 25 - .button-row { 26 - display: flex; 27 - gap: var(--wa-space-s); 28 - flex-wrap: wrap; 29 - } 1 + @import "../../styles/diffuse/facet.css"; 30 2 31 - .dialog-body { 3 + .connect-list { 32 4 display: flex; 33 5 flex-direction: column; 34 - gap: var(--wa-space-m); 35 - } 36 - 37 - .dialog-footer { 38 - display: flex; 39 - gap: var(--wa-space-s); 40 - } 41 - 42 - .connect-list { 6 + gap: var(--space-xs); 43 7 list-style: none; 44 - padding: 0; 45 8 margin: 0; 46 - display: flex; 47 - flex-direction: column; 48 - gap: var(--wa-space-s); 9 + padding: 0; 49 10 } 50 11 51 12 .connect-item { 52 - display: flex; 53 13 align-items: center; 54 - gap: var(--wa-space-s); 14 + display: flex; 15 + gap: var(--space-xs); 55 16 margin-left: 0; 56 17 } 57 18 58 19 .connect-item__info { 59 20 display: flex; 60 21 flex-direction: column; 61 - gap: var(--wa-space-2xs); 62 22 flex: 1; 23 + gap: var(--space-3xs); 63 24 min-width: 0; 64 25 } 65 26 66 27 .connect-item__name { 67 - font-weight: var(--wa-font-weight-semibold); 68 - white-space: nowrap; 28 + font-weight: 600; 69 29 overflow: hidden; 70 30 text-overflow: ellipsis; 31 + white-space: nowrap; 71 32 } 72 33 73 34 .connect-item__detail { 74 - font-size: var(--wa-font-size-xs); 75 - color: var(--wa-color-text-quiet); 76 - white-space: nowrap; 35 + color: oklch(from var(--text-color) l c h / 0.55); 36 + font-size: var(--fs-xs); 77 37 overflow: hidden; 78 38 text-overflow: ellipsis; 39 + white-space: nowrap; 79 40 } 80 41 81 42 .connect-item__tags { 82 43 display: flex; 83 - gap: var(--wa-space-2xs); 84 44 flex-shrink: 0; 45 + gap: var(--space-3xs); 85 46 } 86 47 87 48 .dropzone { 88 49 align-items: center; 89 - border: 2px dashed var(--wa-color-neutral-border-quiet); 90 - border-radius: var(--wa-border-radius-m); 91 - color: var(--wa-color-text-quiet); 50 + border: 2px dashed var(--border-color); 51 + border-radius: var(--radius-md); 52 + color: oklch(from var(--text-color) l c h / 0.55); 53 + cursor: pointer; 92 54 display: flex; 93 55 flex-direction: column; 94 - gap: var(--wa-space-xs); 95 - font-size: var(--wa-font-size-s); 56 + font-size: var(--fs-sm); 57 + gap: var(--space-2xs); 96 58 justify-content: center; 97 - padding: var(--wa-space-l); 59 + padding: var(--space-md); 98 60 transition: 99 61 background-color 150ms, 100 62 border-color 150ms, 101 63 color 150ms; 102 64 103 65 &.dropzone--active { 104 - background-color: var(--wa-color-surface-sunken); 105 - border-color: var(--wa-color-brand-500); 106 - color: var(--wa-color-text-normal); 66 + background-color: var(--form-color); 67 + border-color: var(--accent); 68 + color: var(--text-color); 107 69 } 108 70 } 109 - 110 - [hidden] { 111 - display: none !important; 112 - } 113 - 114 - p { 115 - margin: 0; 116 - }
+64 -78
src/facets/connect/common.js
··· 1 - import "@awesome.me/webawesome/dist/components/badge/badge.js"; 2 - import "@awesome.me/webawesome/dist/components/button/button.js"; 3 - import "@awesome.me/webawesome/dist/components/callout/callout.js"; 4 - import "@awesome.me/webawesome/dist/components/card/card.js"; 5 - import "@awesome.me/webawesome/dist/components/dialog/dialog.js"; 6 - import "@awesome.me/webawesome/dist/components/divider/divider.js"; 7 - import "@awesome.me/webawesome/dist/components/icon/icon.js"; 8 - 9 - import "~/common/webawesome/detect-dark.js"; 10 - import "~/common/webawesome/phosphor/bold.js"; 11 - import "~/common/webawesome/phosphor/fill.js"; 12 - 13 1 import { html, nothing, render as litRender } from "lit-html"; 14 2 15 3 /** 16 - * @import { default as WaDialog } from "@awesome.me/webawesome/dist/components/dialog/dialog.js" 17 4 * @import { TemplateResult } from "lit-html" 18 5 */ 19 6 ··· 28 15 * 29 16 * @param {Object} config 30 17 * @param {string} config.title - Card header title 31 - * @param {TemplateResult | string} config.description - Content above the buttons 18 + * @param {TemplateResult | string} config.description - Content shown on the left side 19 + * @param {TemplateResult} [config.rightContent] - Extra content shown at the top of the right side 32 20 * @param {TemplateResult} config.formFields - Form body content (inputs, footnotes, etc.) 33 21 * @param {(mode: 'input' | 'output') => Promise<void>} config.onSubmit 34 22 * @param {boolean} [config.hasInput] - Whether to show the "Add audio input" button (default: true) ··· 41 29 { 42 30 title, 43 31 description, 32 + rightContent = nothing, 44 33 formFields, 45 34 onSubmit, 46 35 hasInput = true, ··· 53 42 54 43 litRender( 55 44 html` 56 - <wa-card> 57 - <div slot="header" class="card-header"> 58 - <strong>${title}</strong> 45 + <div class="facet__left"> 46 + <div> 47 + <a href="./dashboard/" class="diffuse-logo-container"> 48 + <svg viewBox="0 0 902 134" width="160"> 49 + <title>Diffuse</title> 50 + <use 51 + xlink:href="images/diffuse-current.svg#diffuse" 52 + href="images/diffuse-current.svg#diffuse" 53 + ></use> 54 + </svg> 55 + </a> 59 56 </div> 60 - <div class="card-body"> 61 - ${description} 62 - <div class="button-row"> 63 - ${hasInput 64 - ? html` 65 - <wa-button id="connect-add-input-btn" variant="neutral" appearance="filled"> 66 - <wa-icon slot="start" library="phosphor/fill" name="music-notes"></wa-icon> 67 - Add audio input 68 - </wa-button> 69 - ` 70 - : nothing} 71 - ${hasOutput 72 - ? html` 73 - <wa-button 74 - id="connect-add-output-btn" 75 - variant="brand" 76 - appearance="filled" 77 - > 78 - <wa-icon slot="start" library="phosphor/fill" name="person"></wa-icon> 79 - Use as userdata storage 80 - </wa-button> 81 - ` 82 - : nothing} 83 - </div> 84 - <wa-callout id="connect-card-error" variant="danger" hidden></wa-callout> 85 - <wa-divider id="connect-divider" hidden></wa-divider> 86 - <ul id="connect-list" class="connect-list" hidden></ul> 57 + <h1>${title}</h1> 58 + ${description} 59 + </div> 60 + <div class="facet__right"> 61 + ${rightContent} 62 + <div class="button-row"> 63 + ${hasInput 64 + ? html` 65 + <button id="connect-add-input-btn"> 66 + <i class="ph-fill ph-music-notes"></i> 67 + Add audio input 68 + </button> 69 + ` 70 + : nothing} 71 + ${hasOutput 72 + ? html` 73 + <button id="connect-add-output-btn" class="button--brand"> 74 + <i class="ph-fill ph-person"></i> 75 + Use as userdata storage 76 + </button> 77 + ` 78 + : nothing} 87 79 </div> 88 - </wa-card> 80 + <div id="connect-card-error" class="callout callout--danger" hidden></div> 81 + <hr id="connect-divider" hidden> 82 + <ul id="connect-list" class="connect-list" hidden></ul> 83 + </div> 89 84 90 - <wa-dialog id="connect-dialog" label=""> 85 + <dialog id="connect-dialog"> 86 + <div class="dialog-header"> 87 + <strong id="connect-dialog-title"></strong> 88 + </div> 91 89 <form id="connect-form" class="dialog-body"> 92 90 ${formFields} 93 - <wa-callout id="connect-error" variant="danger" hidden></wa-callout> 91 + <div id="connect-error" class="callout callout--danger" hidden></div> 94 92 </form> 95 - <div slot="footer" class="dialog-footer"> 96 - <wa-button 97 - id="connect-submit-btn" 98 - type="submit" 99 - form="connect-form" 100 - variant="brand" 101 - appearance="filled" 102 - >Add</wa-button> 103 - <wa-button id="connect-cancel-btn" variant="neutral" appearance="outlined"> 104 - Cancel 105 - </wa-button> 93 + <div class="dialog-footer"> 94 + <button id="connect-submit-btn" type="submit" form="connect-form" class="button--brand">Add</button> 95 + <button id="connect-cancel-btn" type="button">Cancel</button> 106 96 </div> 107 - </wa-dialog> 97 + </dialog> 108 98 `, 109 99 main, 110 100 ); 111 101 112 102 const dialog = 113 - /** @type {WaDialog} */ (main.querySelector("#connect-dialog")); 103 + /** @type {HTMLDialogElement} */ (main.querySelector("#connect-dialog")); 104 + const dialogTitleEl = 105 + /** @type {HTMLElement} */ (main.querySelector("#connect-dialog-title")); 114 106 const form = 115 107 /** @type {HTMLFormElement} */ (main.querySelector("#connect-form")); 116 108 const dialogErrorEl = ··· 144 136 /** @param {'input' | 'output'} m */ 145 137 const openDialog = (m) => { 146 138 mode = m; 147 - dialog.label = m === "input" 139 + dialogTitleEl.textContent = m === "input" 148 140 ? "Add audio input" 149 141 : "Use as userdata storage"; 150 142 form.reset(); 151 143 setDialogError(null); 152 - dialog.open = true; 144 + dialog.showModal(); 153 145 }; 154 146 155 147 if (hasInput) { ··· 170 162 171 163 main.querySelector("#connect-cancel-btn")?.addEventListener("click", () => { 172 164 setDialogError(null); 173 - dialog.open = false; 165 + dialog.close(); 174 166 }); 175 167 176 168 const submitBtn = ··· 183 175 submitBtn.textContent = "Loading …"; 184 176 try { 185 177 await onSubmit(mode); 186 - dialog.open = false; 178 + dialog.close(); 187 179 } catch (err) { 188 180 setDialogError( 189 181 err instanceof Error ? err.message : "Something went wrong", ··· 220 212 </div> 221 213 <div class="connect-item__tags"> 222 214 ${isInput 223 - ? html` 224 - <wa-badge appearance="outlined" variant="neutral">Input</wa-badge> 225 - ` 226 - : nothing} ${isOutput 227 - ? html` 228 - <wa-badge appearance="outlined" variant="${isSelectedOutput 229 - ? "brand" 230 - : "warning"}">Output</wa-badge> 231 - ` 215 + ? html`<span class="badge">Input</span>` 216 + : nothing} 217 + ${isOutput 218 + ? html`<span class="badge ${isSelectedOutput ? "badge--brand" : "badge--warning"}">Output</span>` 232 219 : nothing} 233 220 </div> 234 - <wa-button 235 - appearance="plain" 236 - size="small" 221 + <button 222 + class="button--plain button--small" 237 223 aria-label="Remove" 238 224 @click="${onRemove}" 239 225 > 240 - <wa-icon library="phosphor/bold" name="x"></wa-icon> 241 - </wa-button> 226 + <i class="ph-bold ph-x"></i> 227 + </button> 242 228 </li> 243 229 `, 244 230 )}
+6 -4
src/facets/connect/https/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - @import "./facets/connect/common.css" layer(connect); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./facets/connect/common.css"; 4 6 5 - @layer base, diffuse, wa; 7 + @layer base, diffuse; 6 8 </style> 7 9 8 - <main class="wa-theme-default"></main> 10 + <main></main> 9 11 10 12 <script type="module" src="facets/connect/https/index.inline.js"></script>
+2 -14
src/facets/connect/https/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/input/input.js"; 2 - 3 1 import * as TID from "@atcute/tid"; 4 2 import { html } from "lit-html"; 5 3 ··· 10 8 import foundation from "~/common/foundation.js"; 11 9 12 10 import { setup } from "~/facets/connect/common.js"; 13 - 14 - /** 15 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 16 - */ 17 11 18 12 foundation.setup({ title: "Connect HTTPS | Diffuse" }); 19 13 ··· 47 41 `, 48 42 49 43 formFields: html` 50 - <wa-input 51 - id="https-url" 52 - label="URL" 53 - type="url" 54 - placeholder="https://example.com/audio.mp3" 55 - required 56 - ></wa-input> 44 + <label>URL <input id="https-url" type="url" placeholder="https://example.com/audio.mp3" required></label> 57 45 `, 58 46 59 47 onSubmit: () => addUrl(), 60 48 }); 61 49 62 - const urlInput = /** @type {WaInput} */ (document.querySelector("#https-url")); 50 + const urlInput = /** @type {HTMLInputElement} */ (document.querySelector("#https-url")); 63 51 64 52 //////////////////////////////////////////// 65 53 // REACTIVE LIST
+6 -4
src/facets/connect/icecast/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - @import "./facets/connect/common.css" layer(connect); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./facets/connect/common.css"; 4 6 5 - @layer base, diffuse, wa; 7 + @layer base, diffuse; 6 8 </style> 7 9 8 - <main class="wa-theme-default"></main> 10 + <main></main> 9 11 10 12 <script type="module" src="facets/connect/icecast/index.inline.js"></script>
+2 -14
src/facets/connect/icecast/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/input/input.js"; 2 - 3 1 import * as TID from "@atcute/tid"; 4 2 import { html } from "lit-html"; 5 3 ··· 10 8 import foundation from "~/common/foundation.js"; 11 9 12 10 import { setup } from "~/facets/connect/common.js"; 13 - 14 - /** 15 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 16 - */ 17 11 18 12 foundation.setup({ title: "Connect Icecast | Diffuse" }); 19 13 ··· 47 41 `, 48 42 49 43 formFields: html` 50 - <wa-input 51 - id="icecast-url" 52 - label="Stream URL" 53 - type="url" 54 - placeholder="https://example.com/stream" 55 - required 56 - ></wa-input> 44 + <label>Stream URL <input id="icecast-url" type="url" placeholder="https://example.com/stream" required></label> 57 45 `, 58 46 59 47 onSubmit: () => addStream(), 60 48 }); 61 49 62 50 const urlInput = 63 - /** @type {WaInput} */ (document.querySelector("#icecast-url")); 51 + /** @type {HTMLInputElement} */ (document.querySelector("#icecast-url")); 64 52 65 53 //////////////////////////////////////////// 66 54 // REACTIVE LIST
+12 -10
src/facets/connect/index.inline.js
··· 37 37 litRender( 38 38 html` 39 39 <div class="connect-index__left"> 40 - <a href="./dashboard/" class="diffuse-logo-container"> 41 - <svg viewBox="0 0 902 134" width="160"> 42 - <title>Diffuse</title> 43 - <use 44 - xlink:href="images/diffuse-current.svg#diffuse" 45 - href="images/diffuse-current.svg#diffuse" 46 - > 47 - </use> 48 - </svg> 49 - </a> 40 + <div> 41 + <a href="./dashboard/" class="diffuse-logo-container"> 42 + <svg viewBox="0 0 902 134" width="160"> 43 + <title>Diffuse</title> 44 + <use 45 + xlink:href="images/diffuse-current.svg#diffuse" 46 + href="images/diffuse-current.svg#diffuse" 47 + > 48 + </use> 49 + </svg> 50 + </a> 51 + </div> 50 52 <h1>Connect</h1> 51 53 <p> 52 54 These are some of the options available to add as an audio source, or to use
+6 -4
src/facets/connect/local/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - @import "./facets/connect/common.css" layer(connect); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./facets/connect/common.css"; 4 6 5 - @layer base, diffuse, wa; 7 + @layer base, diffuse; 6 8 </style> 7 9 8 - <main class="wa-theme-default"></main> 10 + <main></main> 9 11 10 12 <script type="module" src="facets/connect/local/index.inline.js"></script>
+22 -22
src/facets/connect/local/index.inline.js
··· 49 49 50 50 description: html` 51 51 <p>Add local directories or files as audio input.</p> 52 + `, 52 53 54 + rightContent: html` 53 55 <label class="dropzone" id="local-dropzone"> 54 - <input id="local-dropzone-input" type="file" multiple hidden /> 55 - <wa-icon library="phosphor/bold" name="upload-simple"></wa-icon> 56 + <input id="local-dropzone-input" type="file" accept="audio/*" multiple hidden /> 57 + <i class="ph-bold ph-upload-simple"></i> 56 58 <span>Drop or click to select files</span> 57 59 </label> 58 60 59 - <wa-divider id="local-ephemeral-divider" hidden></wa-divider> 61 + <hr id="local-ephemeral-divider" hidden> 60 62 <div id="local-ephemeral-row" class="button-row" hidden> 61 - <wa-button 63 + <button 62 64 id="local-clear-ephemeral-btn" 63 - variant="danger" 64 - appearance="outlined" 65 - size="small" 65 + class="button--danger button--small" 66 66 style="width: 100%" 67 67 > 68 - <wa-icon slot="start" library="phosphor/bold" name="trash"></wa-icon> 68 + <i class="ph-bold ph-trash"></i> 69 69 Clear files 70 - </wa-button> 70 + </button> 71 71 </div> 72 72 73 73 ${supported 74 74 ? html` 75 - <wa-divider></wa-divider> 75 + <hr> 76 76 77 77 <div class="button-row"> 78 - <wa-button id="local-add-dir-btn" variant="neutral" appearance="filled"> 79 - <wa-icon slot="start" library="phosphor/fill" name="folder-open"></wa-icon> 78 + <button id="local-add-dir-btn"> 79 + <i class="ph-fill ph-folder-open"></i> 80 80 Add directory 81 - </wa-button> 82 - <wa-button id="local-add-files-btn" variant="neutral" appearance="filled"> 83 - <wa-icon slot="start" library="phosphor/fill" name="music-notes"></wa-icon> 81 + </button> 82 + <button id="local-add-files-btn"> 83 + <i class="ph-fill ph-music-notes"></i> 84 84 Add files 85 - </wa-button> 85 + </button> 86 86 </div> 87 87 ` 88 88 : nothing} 89 89 `, 90 90 91 - formFields: html` 92 - 93 - `, 91 + formFields: html``, 94 92 onSubmit: async () => {}, 95 93 }); 96 94 ··· 113 111 )); 114 112 115 113 dropzoneInput?.addEventListener("change", async () => { 116 - const files = Array.from(dropzoneInput.files ?? []); 114 + const files = Array.from(dropzoneInput.files ?? []).filter((f) => 115 + f.type.startsWith("audio/") 116 + ); 117 117 dropzoneInput.value = ""; 118 118 if (files.length === 0) return; 119 119 await cacheFiles(files); ··· 323 323 files.push(...dirFiles); 324 324 } else { 325 325 const file = item.getAsFile(); 326 - if (file) files.push(file); 326 + if (file?.type.startsWith("audio/")) files.push(file); 327 327 } 328 328 }), 329 329 ); ··· 362 362 (res, rej) => 363 363 /** @type {FileSystemFileEntry} */ (entry).file(res, rej), 364 364 ); 365 - files.push(file); 365 + if (file.type.startsWith("audio/")) files.push(file); 366 366 } 367 367 }), 368 368 );
+6 -4
src/facets/connect/opensubsonic/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - @import "./facets/connect/common.css" layer(connect); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./facets/connect/common.css"; 4 6 5 - @layer base, diffuse, wa; 7 + @layer base, diffuse; 6 8 </style> 7 9 8 - <main class="wa-theme-default"></main> 10 + <main></main> 9 11 10 12 <script type="module" src="facets/connect/opensubsonic/index.inline.js"></script>
+18 -27
src/facets/connect/opensubsonic/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/input/input.js"; 2 - import "@awesome.me/webawesome/dist/components/select/select.js"; 3 - import "@awesome.me/webawesome/dist/components/option/option.js"; 4 - 5 1 import * as TID from "@atcute/tid"; 6 2 import { html } from "lit-html"; 7 3 ··· 18 14 import { setup } from "~/facets/connect/common.js"; 19 15 20 16 /** 21 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 22 - * @import { default as WaSelect } from "@awesome.me/webawesome/dist/components/select/select.js" 23 17 * @import { Server } from "~/components/input/opensubsonic/types.d.ts" 24 18 */ 25 19 ··· 54 48 <p> 55 49 Connect to an OpenSubsonic server to use it as audio input. 56 50 </p> 57 - <p class="wa-caption-xs"> 51 + <p class="caption"> 58 52 Supports authentication via username + password or an API key. 59 53 </p> 60 54 `, 61 55 62 56 formFields: html` 63 - <wa-input 64 - id="oss-host" 65 - label="Host" 66 - placeholder="music.example.com" 67 - required 68 - ></wa-input> 69 - <wa-select id="oss-tls" label="Use HTTPS / TLS?" value="true"> 70 - <wa-option value="true">Yes</wa-option> 71 - <wa-option value="false">No</wa-option> 72 - </wa-select> 73 - <wa-input id="oss-username" label="Username"></wa-input> 74 - <wa-input id="oss-password" label="Password" type="password"></wa-input> 75 - <p class="wa-caption-xs">Or use an API key instead of username + password:</p> 76 - <wa-input id="oss-apikey" label="API key" type="password"></wa-input> 77 - <p class="wa-caption-xs">* Host is required</p> 57 + <label>Host <input id="oss-host" placeholder="music.example.com" required></label> 58 + <label>Use HTTPS / TLS? 59 + <select id="oss-tls"> 60 + <option value="true" selected>Yes</option> 61 + <option value="false">No</option> 62 + </select> 63 + </label> 64 + <label>Username <input id="oss-username"></label> 65 + <label>Password <input id="oss-password" type="password"></label> 66 + <p class="caption">Or use an API key instead of username + password:</p> 67 + <label>API key <input id="oss-apikey" type="password"></label> 68 + <p class="caption">* Host is required</p> 78 69 `, 79 70 80 71 onSubmit: () => addServer(), 81 72 }); 82 73 83 - const hostInput = /** @type {WaInput} */ (document.querySelector("#oss-host")); 84 - const tlsSelect = /** @type {WaSelect} */ (document.querySelector("#oss-tls")); 74 + const hostInput = /** @type {HTMLInputElement} */ (document.querySelector("#oss-host")); 75 + const tlsSelect = /** @type {HTMLSelectElement} */ (document.querySelector("#oss-tls")); 85 76 const usernameInput = 86 - /** @type {WaInput} */ (document.querySelector("#oss-username")); 77 + /** @type {HTMLInputElement} */ (document.querySelector("#oss-username")); 87 78 const passwordInput = 88 - /** @type {WaInput} */ (document.querySelector("#oss-password")); 79 + /** @type {HTMLInputElement} */ (document.querySelector("#oss-password")); 89 80 const apikeyInput = 90 - /** @type {WaInput} */ (document.querySelector("#oss-apikey")); 81 + /** @type {HTMLInputElement} */ (document.querySelector("#oss-apikey")); 91 82 92 83 //////////////////////////////////////////// 93 84 // REACTIVE LIST
+6 -4
src/facets/connect/s3/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - @import "./facets/connect/common.css" layer(connect); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./facets/connect/common.css"; 4 6 5 - @layer base, diffuse, wa; 7 + @layer base, diffuse; 6 8 </style> 7 9 8 - <main class="wa-theme-default"></main> 10 + <main></main> 9 11 10 12 <script type="module" src="facets/connect/s3/index.inline.js"></script>
+14 -27
src/facets/connect/s3/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/input/input.js"; 2 - 3 1 import * as TID from "@atcute/tid"; 4 2 import { html } from "lit-html"; 5 3 ··· 15 13 foundation.setup({ title: "Connect S3 | Diffuse" }); 16 14 17 15 /** 18 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 19 16 * @import { Bucket } from "~/components/input/s3/types.d.ts" 20 17 * @import { S3OutputElement } from "~/components/output/bytes/s3/types.d.ts" 21 18 */ ··· 63 60 Connect to an S3-compatible storage to use it as audio input or user-data 64 61 storage. 65 62 </p> 66 - <p class="wa-caption-xs"> 63 + <p class="caption"> 67 64 A custom syncing strategy is used for the user-data storage, tracking what was 68 65 added and removed so conflicts can be resolved. 69 66 </p> 70 67 `, 71 68 72 69 formFields: html` 73 - <wa-input id="s3-access-key" label="Access key" required></wa-input> 74 - <wa-input 75 - id="s3-secret-key" 76 - label="Secret key" 77 - type="password" 78 - required 79 - ></wa-input> 80 - <wa-input 81 - id="s3-bucket-name" 82 - label="Bucket name" 83 - placeholder="my-bucket" 84 - required 85 - ></wa-input> 86 - <wa-input id="s3-host" label="Host" placeholder="s3.amazonaws.com"></wa-input> 87 - <wa-input id="s3-region" label="Region" placeholder="us-east-1"></wa-input> 88 - <wa-input id="s3-path" label="Path" placeholder="/"></wa-input> 89 - <p class="wa-caption-xs">* Required fields</p> 70 + <label>Access key <input id="s3-access-key" required></label> 71 + <label>Secret key <input id="s3-secret-key" type="password" required></label> 72 + <label>Bucket name <input id="s3-bucket-name" placeholder="my-bucket" required></label> 73 + <label>Host <input id="s3-host" placeholder="s3.amazonaws.com"></label> 74 + <label>Region <input id="s3-region" placeholder="us-east-1"></label> 75 + <label>Path <input id="s3-path" placeholder="/"></label> 76 + <p class="caption">* Required fields</p> 90 77 `, 91 78 92 79 onSubmit: (mode) => addBucket(mode), ··· 97 84 }); 98 85 99 86 const accessKeyInput = 100 - /** @type {WaInput} */ (document.querySelector("#s3-access-key")); 87 + /** @type {HTMLInputElement} */ (document.querySelector("#s3-access-key")); 101 88 const secretKeyInput = 102 - /** @type {WaInput} */ (document.querySelector("#s3-secret-key")); 89 + /** @type {HTMLInputElement} */ (document.querySelector("#s3-secret-key")); 103 90 const bucketNameInput = 104 - /** @type {WaInput} */ (document.querySelector("#s3-bucket-name")); 105 - const hostInput = /** @type {WaInput} */ (document.querySelector("#s3-host")); 91 + /** @type {HTMLInputElement} */ (document.querySelector("#s3-bucket-name")); 92 + const hostInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-host")); 106 93 const regionInput = 107 - /** @type {WaInput} */ (document.querySelector("#s3-region")); 108 - const pathInput = /** @type {WaInput} */ (document.querySelector("#s3-path")); 94 + /** @type {HTMLInputElement} */ (document.querySelector("#s3-region")); 95 + const pathInput = /** @type {HTMLInputElement} */ (document.querySelector("#s3-path")); 109 96 110 97 //////////////////////////////////////////// 111 98 // REACTIVE LIST
+49 -76
src/facets/misc/scrobble/last.fm/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - 4 - @layer base, diffuse, wa; 5 - 6 - #container { 7 - display: flex; 8 - align-items: center; 9 - justify-content: center; 10 - min-height: 100dvh; 11 - margin: 0; 12 - } 13 - 14 - 15 - wa-card { 16 - width: min(360px, calc(100vw - 2rem)); 17 - } 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./styles/diffuse/facet.css"; 18 5 19 - .card-header { 20 - display: flex; 21 - align-items: center; 22 - justify-content: space-between; 23 - } 6 + @layer base, diffuse; 24 7 25 - .card-body { 8 + .facet__right { 26 9 display: flex; 27 10 flex-direction: column; 28 - gap: var(--wa-space-m); 29 - } 30 - 31 - .drawer-body { 32 - display: flex; 33 - flex-direction: column; 34 - gap: var(--wa-space-m); 35 - } 36 - 37 - .drawer-footer { 38 - display: flex; 39 - gap: var(--wa-space-s); 40 - } 41 - 42 - [hidden] { 43 - display: none !important; 44 - } 45 - 46 - p { 47 - margin: 0; 11 + gap: var(--space-sm); 12 + width: 24rem; 48 13 } 49 14 </style> 50 15 51 - <main class="wa-theme-default"> 52 - <wa-card> 53 - <div slot="header" class="card-header"> 54 - <strong>Last.fm</strong> 55 - <!--<wa-button id="settings-btn" appearance="plain" size="small" aria-label="API credentials"> 56 - <wa-icon name="gear"></wa-icon> 57 - </wa-button>--> 16 + <main> 17 + <div class="facet__left"> 18 + <div> 19 + <a href="./dashboard/" class="diffuse-logo-container"> 20 + <svg viewBox="0 0 902 134" width="160"> 21 + <title>Diffuse</title> 22 + <use 23 + xlink:href="images/diffuse-current.svg#diffuse" 24 + href="images/diffuse-current.svg#diffuse" 25 + ></use> 26 + </svg> 27 + </a> 58 28 </div> 29 + <h1>Last.fm</h1> 30 + <p>Connect your Last.fm account to scrobble tracks.</p> 31 + </div> 59 32 60 - <div id="state-connect" class="card-body"> 61 - <p>Connect your Last.fm account to start scrobbling.</p> 62 - <wa-button id="sign-in-btn" variant="brand" appearance="filled"> 63 - <wa-icon slot="start" library="phosphor/bold" name="plugs"></wa-icon> 64 - Connect 65 - </wa-button> 33 + <div class="facet__right"> 34 + <div id="state-connect"> 35 + <div class="button-row"> 36 + <button id="sign-in-btn" class="button--brand"> 37 + <i class="ph-bold ph-plugs"></i> 38 + Connect 39 + </button> 40 + </div> 66 41 </div> 67 42 68 - <div id="state-connected" class="card-body" hidden> 43 + <div id="state-connected" hidden> 69 44 <p id="handle-paragraph" hidden>Connected as <strong id="handle-text"></strong>.</p> 70 - <wa-button id="sign-out-btn" variant="neutral" appearance="outlined" hidden> 71 - <wa-icon slot="start" library="phosphor/bold" name="plugs-connecte"></wa-icon> 72 - Disconnect 73 - </wa-button> 45 + <div class="button-row"> 46 + <button id="sign-out-btn" hidden> 47 + <i class="ph-bold ph-plugs-connected"></i> 48 + Disconnect 49 + </button> 50 + </div> 74 51 </div> 75 - </wa-card> 52 + </div> 76 53 77 - <wa-drawer id="credentials-drawer" label="API Credentials" placement="end"> 78 - <div class="drawer-body"> 79 - <wa-input id="api-key-input" label="API Key" placeholder="Default"></wa-input> 80 - <wa-input 81 - id="api-secret-input" 82 - label="API Secret" 83 - type="password" 84 - placeholder="Default" 85 - ></wa-input> 54 + <dialog id="credentials-dialog"> 55 + <div class="dialog-header"> 56 + <strong>API Credentials</strong> 57 + </div> 58 + <div class="dialog-body"> 59 + <label>API Key <input id="api-key-input" placeholder="Default"></label> 60 + <label>API Secret <input id="api-secret-input" type="password" placeholder="Default"></label> 86 61 </div> 87 - <div slot="footer" class="drawer-footer"> 88 - <wa-button id="save-creds-btn" variant="brand" appearance="filled">Save</wa-button> 89 - <wa-button id="reset-creds-btn" variant="neutral" appearance="outlined" 90 - >Reset to defaults</wa-button 91 - > 62 + <div class="dialog-footer"> 63 + <button id="save-creds-btn" class="button--brand">Save</button> 64 + <button id="reset-creds-btn">Reset to defaults</button> 92 65 </div> 93 - </wa-drawer> 66 + </dialog> 94 67 </main> 95 68 96 69 <script type="module" src="facets/misc/scrobble/last.fm/index.inline.js"></script>
+6 -26
src/facets/misc/scrobble/last.fm/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/card/card.js"; 2 - import "@awesome.me/webawesome/dist/components/button/button.js"; 3 - import "@awesome.me/webawesome/dist/components/drawer/drawer.js"; 4 - import "@awesome.me/webawesome/dist/components/input/input.js"; 5 - import "@awesome.me/webawesome/dist/components/icon/icon.js"; 6 - 7 - import "~/common/webawesome/detect-dark.js"; 8 - import "~/common/webawesome/phosphor/bold.js"; 9 - 10 1 import foundation from "~/common/foundation.js"; 11 2 import { effect } from "~/common/signal.js"; 12 3 13 4 // Set doc title 14 5 foundation.setup({ title: "Last.fm | Scrobble | Diffuse" }); 15 - 16 - /** 17 - * @import { default as WaDrawer } from "@awesome.me/webawesome/dist/components/drawer/drawer.js" 18 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 19 - */ 20 6 21 7 //////////////////////////////////////////// 22 8 // SETUP ··· 47 33 configurator.append(lastFm); 48 34 } 49 35 50 - // const creds = loadCredentials(); 51 - // if (creds) { 52 - // lastFm.setAttribute("api-key", creds.apiKey); 53 - // lastFm.setAttribute("api-secret", creds.apiSecret); 54 - // } 55 - 56 36 await customElements.whenDefined(lastFm.localName); 57 37 58 38 //////////////////////////////////////////// ··· 87 67 document.querySelector("#sign-out-btn") 88 68 ); 89 69 90 - const credentialsDrawer = /** @type {WaDrawer} */ ( 91 - document.querySelector("#credentials-drawer") 70 + const credentialsDialog = /** @type {HTMLDialogElement} */ ( 71 + document.querySelector("#credentials-dialog") 92 72 ); 93 73 94 - const apiKeyInput = /** @type {WaInput} */ ( 74 + const apiKeyInput = /** @type {HTMLInputElement} */ ( 95 75 document.querySelector("#api-key-input") 96 76 ); 97 77 98 - const apiSecretInput = /** @type {WaInput} */ ( 78 + const apiSecretInput = /** @type {HTMLInputElement} */ ( 99 79 document.querySelector("#api-secret-input") 100 80 ); 101 81 ··· 107 87 document.querySelector("#reset-creds-btn") 108 88 ); 109 89 110 - // Pre-fill drawer inputs with stored credentials 90 + // Pre-fill dialog inputs with stored credentials 111 91 const existingCreds = loadCredentials(); 112 92 if (existingCreds) { 113 93 apiKeyInput.value = existingCreds.apiKey; ··· 140 120 141 121 settingsBtn?.addEventListener("click", (e) => { 142 122 e.stopPropagation(); 143 - credentialsDrawer.open = true; 123 + credentialsDialog.showModal(); 144 124 }); 145 125 146 126 signInBtn.onclick = () => lastFm.signIn();
+43 -52
src/facets/misc/scrobble/listenbrainz/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 - 4 - @layer base, diffuse, wa; 5 - 6 - #container { 7 - display: flex; 8 - align-items: center; 9 - justify-content: center; 10 - min-height: 100dvh; 11 - margin: 0; 12 - } 13 - 14 - wa-card { 15 - width: min(360px, calc(100vw - 2rem)); 16 - } 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./styles/diffuse/facet.css"; 17 5 18 - .card-header { 19 - display: flex; 20 - align-items: center; 21 - justify-content: space-between; 22 - } 6 + @layer base, diffuse; 23 7 24 - .card-body { 8 + .facet__right { 25 9 display: flex; 26 10 flex-direction: column; 27 - gap: var(--wa-space-m); 28 - } 29 - 30 - [hidden] { 31 - display: none !important; 32 - } 33 - 34 - p { 35 - margin: 0; 11 + gap: var(--space-sm); 12 + width: 24rem; 36 13 } 37 14 </style> 38 15 39 - <main class="wa-theme-default"> 40 - <wa-card> 41 - <div slot="header" class="card-header"> 42 - <strong>ListenBrainz</strong> 16 + <main> 17 + <div class="facet__left"> 18 + <div> 19 + <a href="./dashboard/" class="diffuse-logo-container"> 20 + <svg viewBox="0 0 902 134" width="160"> 21 + <title>Diffuse</title> 22 + <use 23 + xlink:href="images/diffuse-current.svg#diffuse" 24 + href="images/diffuse-current.svg#diffuse" 25 + ></use> 26 + </svg> 27 + </a> 43 28 </div> 29 + <h1>ListenBrainz</h1> 30 + <p>Connect your ListenBrainz account to scrobble tracks.</p> 31 + </div> 44 32 45 - <div id="state-connect" class="card-body"> 46 - <p>Enter your ListenBrainz user token to start scrobbling.</p> 47 - <wa-input 48 - id="token-input" 49 - label="User token" 50 - type="password" 51 - help-text="Find your token at listenbrainz.org/settings/" 52 - ></wa-input> 53 - <wa-button id="sign-in-btn" variant="brand" appearance="filled"> 54 - <wa-icon slot="start" library="phosphor/bold" name="plugs"></wa-icon> 55 - Connect 56 - </wa-button> 33 + <div class="facet__right"> 34 + <div id="state-connect"> 35 + <label> 36 + User token 37 + <input id="token-input" type="password" placeholder="Your user token" /> 38 + <span class="caption">Find your token at listenbrainz.org/settings/</span> 39 + </label> 40 + <div class="button-row"> 41 + <button id="sign-in-btn" class="button--brand"> 42 + <i class="ph-bold ph-plugs"></i> 43 + Connect 44 + </button> 45 + </div> 57 46 </div> 58 47 59 - <div id="state-connected" class="card-body" hidden> 48 + <div id="state-connected" hidden> 60 49 <p id="handle-paragraph" hidden>Connected as <strong id="handle-text"></strong>.</p> 61 - <wa-button id="sign-out-btn" variant="neutral" appearance="outlined" hidden> 62 - <wa-icon slot="start" library="phosphor/bold" name="plugs-connected"></wa-icon> 63 - Disconnect 64 - </wa-button> 50 + <div class="button-row"> 51 + <button id="sign-out-btn" hidden> 52 + <i class="ph-bold ph-plugs-connected"></i> 53 + Disconnect 54 + </button> 55 + </div> 65 56 </div> 66 - </wa-card> 57 + </div> 67 58 </main> 68 59 69 60 <script type="module" src="facets/misc/scrobble/listenbrainz/index.inline.js"></script>
+1 -14
src/facets/misc/scrobble/listenbrainz/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/card/card.js"; 2 - import "@awesome.me/webawesome/dist/components/button/button.js"; 3 - import "@awesome.me/webawesome/dist/components/input/input.js"; 4 - import "@awesome.me/webawesome/dist/components/icon/icon.js"; 5 - 6 - import "~/common/webawesome/detect-dark.js"; 7 - import "~/common/webawesome/phosphor/bold.js"; 8 - 9 1 import foundation from "~/common/foundation.js"; 10 2 import { effect } from "~/common/signal.js"; 11 3 12 - /** 13 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 14 - */ 15 - 16 4 //////////////////////////////////////////// 17 5 // SETUP 18 6 //////////////////////////////////////////// ··· 55 43 document.querySelector("#handle-text") 56 44 ); 57 45 58 - const tokenInput = /** @type {WaInput} */ ( 46 + const tokenInput = /** @type {HTMLInputElement} */ ( 59 47 document.querySelector("#token-input") 60 48 ); 61 49 ··· 85 73 86 74 // @ts-ignore 87 75 signInBtn.disabled = isAuthenticating; 88 - // @ts-ignore 89 76 tokenInput.disabled = isAuthenticating; 90 77 }); 91 78
+47 -52
src/facets/misc/scrobble/rocksky/index.html
··· 1 1 <style> 2 - @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 2 + @import "./styles/base.css"; 3 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 4 + @import "./styles/diffuse/facet.css"; 3 5 4 - @layer base, diffuse, wa; 6 + @layer base, diffuse; 5 7 6 - #container { 7 - display: flex; 8 - align-items: center; 9 - justify-content: center; 10 - min-height: 100dvh; 11 - margin: 0; 12 - } 13 - 14 - wa-card { 15 - width: min(360px, calc(100vw - 2rem)); 16 - } 17 - 18 - .card-header { 19 - display: flex; 20 - align-items: center; 21 - justify-content: space-between; 22 - } 23 - 24 - .card-body { 8 + .facet__right { 25 9 display: flex; 26 10 flex-direction: column; 27 - gap: var(--wa-space-m); 28 - } 29 - 30 - [hidden] { 31 - display: none !important; 32 - } 33 - 34 - p { 35 - margin: 0; 11 + gap: var(--space-sm); 12 + width: 24rem; 36 13 } 37 14 </style> 38 15 39 - <main class="wa-theme-default"> 40 - <wa-card> 41 - <div slot="header" class="card-header"> 42 - <strong>Rocksky</strong> 16 + <main> 17 + <div class="facet__left"> 18 + <div> 19 + <a href="./dashboard/" class="diffuse-logo-container"> 20 + <svg viewBox="0 0 902 134" width="160"> 21 + <title>Diffuse</title> 22 + <use 23 + xlink:href="images/diffuse-current.svg#diffuse" 24 + href="images/diffuse-current.svg#diffuse" 25 + ></use> 26 + </svg> 27 + </a> 43 28 </div> 29 + <h1>Rocksky</h1> 30 + <p>Scrobble tracks to Rocksky using your AT Protocol identity.</p> 31 + </div> 44 32 45 - <div id="state-connect" class="card-body"> 46 - <div id="state-no-atproto" class="card-body"> 33 + <div class="facet__right"> 34 + <div id="state-connect"> 35 + <div id="state-no-atproto"> 47 36 <p>Sign in with your AT Protocol identity first, then connect to Rocksky.</p> 48 - <wa-input id="handle-input" label="Handle" placeholder="you.bsky.social"></wa-input> 49 - <wa-button id="atproto-sign-in-btn" variant="neutral" appearance="outlined"> 50 - <wa-icon slot="start" library="phosphor/bold" name="at"></wa-icon> 51 - Sign in with AT Protocol 52 - </wa-button> 37 + <label>Handle <input id="handle-input" placeholder="you.bsky.social" /></label> 38 + <p class="button-row"> 39 + <button id="atproto-sign-in-btn"> 40 + <i class="ph-bold ph-at"></i> 41 + Sign in with AT Protocol 42 + </button> 43 + </p> 53 44 </div> 54 45 55 - <div id="state-has-atproto" class="card-body" hidden> 46 + <div id="state-has-atproto" hidden> 56 47 <p>Connect your Rocksky account using your AT Protocol identity.</p> 57 - <wa-button id="sign-in-btn" variant="brand" appearance="filled"> 58 - <wa-icon slot="start" library="phosphor/bold" name="plugs"></wa-icon> 59 - Connect 60 - </wa-button> 48 + <div class="button-row"> 49 + <button id="sign-in-btn" class="button--brand"> 50 + <i class="ph-bold ph-plugs"></i> 51 + Connect 52 + </button> 53 + </div> 61 54 </div> 62 55 </div> 63 56 64 - <div id="state-connected" class="card-body" hidden> 57 + <div id="state-connected" hidden> 65 58 <p id="handle-paragraph" hidden>Connected as <strong id="handle-text"></strong>.</p> 66 - <wa-button id="sign-out-btn" variant="neutral" appearance="outlined" hidden> 67 - <wa-icon slot="start" library="phosphor/bold" name="plugs-connecte"></wa-icon> 68 - Disconnect 69 - </wa-button> 59 + <div class="button-row"> 60 + <button id="sign-out-btn" hidden> 61 + <i class="ph-bold ph-plugs-connected"></i> 62 + Disconnect 63 + </button> 64 + </div> 70 65 </div> 71 - </wa-card> 66 + </div> 72 67 </main> 73 68 74 69 <script type="module" src="facets/misc/scrobble/rocksky/index.inline.js"></script>
+1 -13
src/facets/misc/scrobble/rocksky/index.inline.js
··· 1 - import "@awesome.me/webawesome/dist/components/card/card.js"; 2 - import "@awesome.me/webawesome/dist/components/button/button.js"; 3 - import "@awesome.me/webawesome/dist/components/input/input.js"; 4 - import "@awesome.me/webawesome/dist/components/icon/icon.js"; 5 - 6 - import "~/common/webawesome/detect-dark.js"; 7 - import "~/common/webawesome/phosphor/bold.js"; 8 - 9 1 import { login } from "~/components/output/raw/atproto/oauth.js"; 10 2 import { finalizeAuthorization } from "@atcute/oauth-browser-client"; 11 3 12 4 import foundation from "~/common/foundation.js"; 13 5 import { effect, signal } from "~/common/signal.js"; 14 - 15 - /** 16 - * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 17 - */ 18 6 19 7 //////////////////////////////////////////// 20 8 // SETUP ··· 92 80 document.querySelector("#handle-text") 93 81 ); 94 82 95 - const handleInput = /** @type {WaInput} */ ( 83 + const handleInput = /** @type {HTMLInputElement} */ ( 96 84 document.querySelector("#handle-input") 97 85 ); 98 86
+255
src/styles/diffuse/facet.css
··· 1 + body { 2 + background-color: var(--bg-color); 3 + color: var(--text-color); 4 + } 5 + 6 + main { 7 + padding: var(--space-2xl) var(--space-xl); 8 + 9 + @media (min-width: 48rem) { 10 + align-items: center; 11 + display: flex; 12 + flex-direction: row; 13 + gap: var(--space-2xl); 14 + justify-content: center; 15 + min-height: 100dvh; 16 + } 17 + } 18 + 19 + .facet__left { 20 + container-type: inline-size; 21 + flex: 1; 22 + margin-bottom: var(--space-lg); 23 + max-width: 24rem; 24 + 25 + @media (min-width: 48rem) { 26 + margin-bottom: 0; 27 + } 28 + } 29 + 30 + .facet__right { 31 + max-width: 24rem; 32 + } 33 + 34 + .diffuse-logo-container { 35 + display: inline-block; 36 + margin-bottom: var(--space-lg); 37 + 38 + svg { 39 + display: block; 40 + fill: oklch(from var(--bg-color) calc(l - 0.5) c h); 41 + opacity: 0.2; 42 + width: 5em; 43 + 44 + @media (prefers-color-scheme: dark) { 45 + fill: var(--text-color); 46 + opacity: 0.25; 47 + } 48 + } 49 + } 50 + 51 + h1 { 52 + font-size: var(--fs-2xl); 53 + font-size: min(14cqi, var(--fs-2xl)); 54 + font-weight: 700; 55 + letter-spacing: -0.02em; 56 + line-height: 1.2; 57 + margin: 0 0 var(--space-sm); 58 + overflow: hidden; 59 + white-space: nowrap; 60 + } 61 + 62 + .facet__left p { 63 + color: oklch(from var(--text-color) l c h / 0.55); 64 + font-size: var(--fs-sm); 65 + line-height: var(--leading-relaxed); 66 + max-width: 22rem; 67 + } 68 + 69 + .button-row { 70 + display: flex; 71 + flex-wrap: wrap; 72 + gap: var(--space-2xs); 73 + } 74 + 75 + .callout { 76 + border-radius: var(--radius-md); 77 + font-size: var(--fs-sm); 78 + padding: var(--space-2xs) var(--space-xs); 79 + } 80 + 81 + .callout--danger { 82 + background: oklch(from var(--accent-twist-4) l c h / 0.12); 83 + border: 1px solid oklch(from var(--accent-twist-4) l c h / 0.3); 84 + color: var(--accent-twist-4); 85 + } 86 + 87 + .callout--warning { 88 + background: oklch(from var(--accent-twist-1) l c h / 0.12); 89 + border: 1px solid oklch(from var(--accent-twist-1) l c h / 0.3); 90 + color: var(--accent-twist-1); 91 + } 92 + 93 + .badge { 94 + border: 1px solid currentColor; 95 + border-radius: var(--radius-xs); 96 + font-size: var(--fs-3xs); 97 + letter-spacing: var(--tracking-wider); 98 + line-height: 0.75; 99 + opacity: 0.6; 100 + padding: 0.1em 0.35em; 101 + text-box: trim-both cap alphabetic; 102 + text-transform: uppercase; 103 + } 104 + 105 + .badge--brand { 106 + color: var(--accent); 107 + opacity: 1; 108 + } 109 + 110 + .badge--warning { 111 + color: var(--accent-twist-1); 112 + opacity: 1; 113 + } 114 + 115 + button { 116 + align-items: center; 117 + background: var(--form-color); 118 + border: 1px solid var(--border-color); 119 + border-radius: 9999px; 120 + color: var(--text-color); 121 + cursor: pointer; 122 + display: inline-flex; 123 + font-family: inherit; 124 + font-size: var(--fs-sm); 125 + gap: var(--space-2xs); 126 + padding: var(--space-2xs) var(--space-sm); 127 + 128 + &:hover:not(:disabled) { 129 + opacity: 0.8; 130 + } 131 + 132 + &:disabled { 133 + cursor: not-allowed; 134 + opacity: 0.5; 135 + } 136 + 137 + &.button--brand { 138 + background: var(--accent); 139 + border-color: transparent; 140 + color: var(--bg-color); 141 + } 142 + 143 + &.button--outlined { 144 + background: transparent; 145 + } 146 + 147 + &.button--danger { 148 + border-color: var(--accent-twist-4); 149 + color: var(--accent-twist-4); 150 + } 151 + 152 + &.button--plain { 153 + background: transparent; 154 + border-color: transparent; 155 + padding: var(--space-3xs); 156 + } 157 + 158 + &.button--small { 159 + font-size: var(--fs-xs); 160 + padding: var(--space-3xs) var(--space-2xs); 161 + } 162 + } 163 + 164 + label { 165 + display: flex; 166 + flex-direction: column; 167 + font-size: var(--fs-sm); 168 + gap: var(--space-3xs); 169 + } 170 + 171 + input, 172 + select { 173 + background: var(--form-color); 174 + border: 1px solid var(--border-color); 175 + border-radius: var(--radius-md); 176 + color: var(--text-color); 177 + font-family: inherit; 178 + font-size: var(--fs-sm); 179 + padding: var(--space-2xs) var(--space-xs); 180 + width: 100%; 181 + } 182 + 183 + dialog { 184 + background: var(--bg-color); 185 + border: 1px solid var(--border-color); 186 + border-radius: var(--radius-lg); 187 + color: var(--text-color); 188 + left: 50%; 189 + max-height: 85dvh; 190 + max-width: min(400px, calc(100vw - 2rem)); 191 + overflow: hidden; 192 + padding: 0; 193 + position: fixed; 194 + top: 50%; 195 + transform: translate(-50%, -50%); 196 + width: 100%; 197 + 198 + &[open] { 199 + display: flex; 200 + flex-direction: column; 201 + } 202 + 203 + &::backdrop { 204 + background: oklch(0 0 0 / 0.5); 205 + } 206 + } 207 + 208 + .dialog-header { 209 + align-items: center; 210 + border-bottom: 1px solid var(--border-color); 211 + display: flex; 212 + flex-shrink: 0; 213 + justify-content: space-between; 214 + padding: var(--space-xs) var(--space-sm); 215 + } 216 + 217 + .dialog-body { 218 + display: flex; 219 + flex-direction: column; 220 + gap: var(--space-sm); 221 + overflow-y: auto; 222 + padding: var(--space-sm); 223 + } 224 + 225 + .dialog-footer { 226 + border-top: 1px solid var(--border-color); 227 + display: flex; 228 + flex-shrink: 0; 229 + gap: var(--space-2xs); 230 + padding: var(--space-xs) var(--space-sm); 231 + } 232 + 233 + hr { 234 + border: none; 235 + border-top: 1px solid var(--border-color); 236 + margin: var(--space-md) 0; 237 + } 238 + 239 + .caption { 240 + color: oklch(from var(--text-color) l c h / 0.55); 241 + font-size: var(--fs-xs); 242 + } 243 + 244 + [hidden] { 245 + display: none !important; 246 + } 247 + 248 + p, 249 + .callout { 250 + margin: var(--space-sm) 0; 251 + } 252 + 253 + .facet__right p:first-child { 254 + margin-top: 0; 255 + }