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.

feat: activate/deactivate storage method

+149 -61
+1 -1
src/common/element.js
··· 12 12 * @import {Signal} from "./signal.d.ts" 13 13 */ 14 14 15 - export { nothing } from "lit-html"; 15 + export { html, nothing } from "lit-html"; 16 16 export const DEFAULT_GROUP = "default"; 17 17 18 18 /**
+5 -5
src/components/configurator/output/element.js
··· 268 268 /** 269 269 * @override 270 270 */ 271 - dependencies() { 271 + dependencies = () => { 272 272 return Object.fromEntries( 273 - Array.from(this.children).flatMap((element) => { 273 + Array.from(this.root().children).flatMap((element) => { 274 274 if (element.hasAttribute("id") === false) { 275 275 console.warn( 276 276 "Missing `id` for output configurator child element with `localName` '" + ··· 287 287 288 288 // ADDITIONAL ACTIONS 289 289 290 - async deselect() { 290 + deselect = async () => { 291 291 localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); 292 292 this.#selectedOutput.value = await this.#findSelectedOutput(); 293 293 } 294 294 295 - async options() { 295 + options = async () => { 296 296 const deps = this.dependencies(); 297 297 const entries = Object.entries(deps); 298 298 ··· 312 312 /** 313 313 * @param {string} id 314 314 */ 315 - async select(id) { 315 + select = async (id) => { 316 316 localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); 317 317 this.#selectedOutput.value = await this.#findSelectedOutput(); 318 318 }
+7 -7
src/components/configurator/output/types.d.ts
··· 3 3 4 4 export type OutputConfiguratorElement = OutputElement & { 5 5 deselect: () => Promise<void>; 6 - options: () => Promise< 7 - Array<{ 8 - id: string; 9 - label: string; 10 - element: OutputElement; 11 - }> 12 - >; 6 + options: () => Promise<Array<OutputOption>>; 13 7 select: (id: string) => Promise<void>; 14 8 selectedOutput: SignalReader<OutputElement | null>; 15 9 }; 10 + 11 + export type OutputOption<ElementType = OutputElement> = { 12 + id: string; 13 + label: string; 14 + element: ElementType; 15 + }
+1
src/components/orchestrator/output/element.js
··· 109 109 <dc-output id="do-output__dc-output" default="do-output__dtos-json"> 110 110 <dtos-json 111 111 id="do-output__dtos-json" 112 + label="IndexedDB as a JSON string" 112 113 output-selector="#do-output__dop-indexed-db__json" 113 114 ></dtos-json> 114 115
+9 -1
src/components/output/raw/atproto/oauth.js
··· 67 67 * @param {string} handle 68 68 */ 69 69 export async function login(handle) { 70 + const location = globalThis.location 71 + 72 + if (location.origin.startsWith("http://localhost")) { 73 + location.assign( 74 + location.href.replace("http://localhost:", "http://127.0.0.1:"), 75 + ) 76 + } 77 + 70 78 const authUrl = await createAuthorizationUrl({ 71 79 target: { type: "account", identifier: /** @type {any} */ (handle) }, 72 80 scope: "atproto transition:generic", 73 81 }); 74 82 75 - globalThis.location.assign(authUrl.toString()); 83 + location.assign(authUrl.toString()); 76 84 } 77 85 78 86 // SESSION RESTORE / CALLBACK
+6
src/facets/l/index.js
··· 32 32 33 33 const container = /** @type {HTMLDivElement} */ (containerNull); 34 34 35 + /** @type {string | null} */ 36 + let loadedCid = null; 37 + 35 38 effect(async () => { 36 39 const collection = output.facets.collection(); 37 40 if (output.facets.state() !== "loaded") return; ··· 72 75 facet.cid = cid; 73 76 } 74 77 78 + if (facet.cid === loadedCid) return; 79 + 80 + loadedCid = facet.cid ?? null; 75 81 loadIntoContainer(facet); 76 82 }); 77 83
+10 -4
src/themes/l/index.js
··· 27 27 // LOAD 28 28 //////////////////////////////////////////// 29 29 30 + /** @type {string | null} */ 31 + let loadedCid = null; 32 + 30 33 effect(async () => { 31 34 const collection = output.themes.collection(); 32 35 if (output.themes.state() !== "loaded") return; ··· 64 67 theme.cid = cid; 65 68 } 66 69 70 + if (theme.cid === loadedCid) return; 71 + 72 + loadedCid = theme.cid ?? null; 67 73 loadIntoContainer(theme); 68 74 }); 69 75 ··· 73 79 function loadIntoContainer(theme) { 74 80 // TODO: Validate if CID matches HTML 75 81 76 - const iframe = document.createElement("iframe") 77 - iframe.srcdoc = theme.html ?? "" 82 + const iframe = document.createElement("iframe"); 83 + iframe.srcdoc = theme.html ?? ""; 78 84 79 - document.body.innerHTML = "" 80 - document.body.append(iframe) 85 + document.body.innerHTML = ""; 86 + document.body.append(iframe); 81 87 }
+105 -41
src/themes/webamp/configurators/output/element.js
··· 1 - import { DiffuseElement, query } from "@common/element.js"; 1 + import { DiffuseElement, nothing, query } from "@common/element.js"; 2 2 import { signal } from "@common/signal.js"; 3 + 3 4 import { NAME as ATPROTO_NAME } from "@components/output/raw/atproto/element.js"; 4 5 5 6 /** 6 7 * @import {ATProtoOutputElement} from "@components/output/raw/atproto/types.d.ts" 7 8 * @import {OutputElement} from "@components/output/types.d.ts" 8 - * @import {OutputConfiguratorElement} from "@components/configurator/output/element.js" 9 + * @import {OutputConfiguratorElement, OutputOption} from "@components/configurator/output/types.d.ts" 9 10 * @import {RenderArg} from "@common/element.d.ts" 10 11 */ 11 12 ··· 21 22 /** @type {OutputElement | OutputConfiguratorElement | undefined} */ (undefined), 22 23 ); 23 24 24 - $atproto = signal(/** @type {ATProtoOutputElement | null} */ (null)); 25 + $atproto = signal( 26 + /** @type {OutputOption<ATProtoOutputElement> | null} */ (null), 27 + ); 25 28 26 29 // LIFECYCLE 27 30 ··· 29 32 async connectedCallback() { 30 33 super.connectedCallback(); 31 34 32 - /** @type {OutputElement} */ 35 + /** @type {OutputElement | OutputConfiguratorElement} */ 33 36 const output = query(this, "output-selector"); 34 37 35 38 await customElements.whenDefined(output.localName); 36 39 37 40 this.$output.value = output; 38 41 39 - // Try setting up ATProto output 40 - await customElements.whenDefined(ATPROTO_NAME); 42 + // Try setting up specific outputs 43 + if ("options" in output === false) return; 44 + const options = await output.options(); 45 + const atproto = options.find((o) => o.element.localName === ATPROTO_NAME); 41 46 42 - /** @type {ATProtoOutputElement | null} */ 43 - const atproto = output.querySelector(ATPROTO_NAME); 44 - if (atproto) this.$atproto.value = atproto; 47 + if (atproto) { 48 + this.$atproto.value = 49 + /** @type {OutputOption<ATProtoOutputElement>} */ (atproto); 50 + } 45 51 } 46 52 47 53 // EVENTS ··· 62 68 const button = this.root().querySelector("#atproto-submit"); 63 69 if (button) button.disabled = true; 64 70 65 - await atproto.login(handle); 71 + await atproto.element.login(handle); 66 72 }; 67 73 68 74 #handleAtprotoLogout = async () => { 69 75 const atproto = this.$atproto.value; 70 76 if (!atproto) return; 71 77 72 - await atproto.logout(); 78 + await atproto.element.logout(); 79 + }; 80 + 81 + #handleAtprotoActivate = async () => { 82 + const output = this.$output.value; 83 + if (!output || !("select" in output)) return; 84 + 85 + const atproto = this.$atproto.value; 86 + if (!atproto) return; 87 + 88 + await output.select(atproto.id); 89 + }; 90 + 91 + #handleDeactivate = async () => { 92 + const output = this.$output.value; 93 + if (!output || !("deselect" in output)) return; 94 + 95 + await output.deselect(); 73 96 }; 74 97 75 98 // RENDER ··· 78 101 * @param {RenderArg} _ 79 102 */ 80 103 render({ html }) { 81 - const did = this.$atproto.value?.did() ?? null; 82 - const selectedOutput = this.$output.value?.selectedOutput(); 104 + const did = this.$atproto.value?.element.did() ?? null; 105 + const selectedOutput = 106 + this.$output.value && "selectedOutput" in this.$output.value 107 + ? this.$output.value.selectedOutput() 108 + : undefined; 83 109 84 110 return html` 85 111 <link rel="stylesheet" href="styles/vendor/98.css" /> ··· 87 113 88 114 <style> 89 115 @import "./themes/webamp/98-vars.css"; 116 + 117 + .button-row { 118 + display: inline-flex; 119 + gap: var(--grouped-button-spacing); 120 + } 90 121 91 122 #tabbed { 92 123 display: flex; ··· 187 218 188 219 <fieldset> 189 220 <legend>Active storage method</legend> 190 - <span class="with-icon with-icon--large"> 221 + <div class="with-icon with-icon--large"> 191 222 <img 192 - src="images/icons/windows_98/msg_warning-0.png" 223 + src="images/icons/windows_98/${selectedOutput 224 + ? `directory_channels-2.png` 225 + : `msg_warning-0.png`}" 193 226 width="24" 194 227 /> 195 - <span> 196 - ${ 197 - this.$output.value && "selectedOutput" in this.$output.value 198 - ? selectedOutput 199 - ? `Selected output: ${selectedOutput.name}` 200 - : this.#defaultOutputMessage 228 + <div> 229 + ${this.$output.value && "selectedOutput" in this.$output.value 230 + ? selectedOutput 231 + ? html` 232 + <p> 233 + Selected output: 234 + <strong>${selectedOutput.label}</strong><br /> 235 + </p> 236 + <p> 237 + <button @click="${this 238 + .#handleDeactivate}">Deactivate</button> 239 + </p> 240 + ` 201 241 : this.#defaultOutputMessage 202 - } 203 - </span> 204 - </span> 242 + : this.#defaultOutputMessage} 243 + </div> 244 + </div> 205 245 </fieldset> 206 246 </div> 207 247 ··· 216 256 </span> 217 257 </fieldset> 218 258 219 - <p> 259 + <p class="button-row"> 220 260 <button @click="${this 221 261 .#handleAtprotoLogout}">Sign out</button> 262 + ${this.#renderAtprotoActivation(html, selectedOutput)} 222 263 </p> 223 264 ` 224 265 : html` ··· 249 290 <button type="submit" id="atproto-submit">Sign in</button> 250 291 </p> 251 292 </form> 252 - `} 253 - </div> 293 + `} 294 + </div> 254 295 255 - <!-- S3 --> 256 - <div class="window-body" id="s3-contents"> 257 - <p>TODO</p> 258 - </div> 296 + <!-- S3 --> 297 + <div class="window-body" id="s3-contents"> 298 + <p>TODO</p> 259 299 </div> 260 300 </div> 261 - `; 262 - } 301 + </div> 302 + `; 303 + } 304 + 305 + /** 306 + * @param {RenderArg['html']} html 307 + * @param {OutputElement | null | undefined} selectedOutput 308 + */ 309 + #renderAtprotoActivation(html, selectedOutput) { 310 + const output = this.$output.value; 311 + if (!output || !("select" in output)) return nothing; 263 312 264 - #defaultOutputMessage = "Storing data locally in the browser without any backup or syncing enabled." 313 + const atproto = this.$atproto.value; 314 + const isActive = selectedOutput && atproto && 315 + selectedOutput.selector === atproto.element.selector; 316 + 317 + return isActive 318 + ? html` 319 + <button @click="${this.#handleDeactivate}">Deactivate</button> 320 + ` 321 + : html` 322 + <button @click="${this 323 + .#handleAtprotoActivate}">Activate this storage</button> 324 + `; 265 325 } 266 326 267 - export default OutputConfig; 327 + #defaultOutputMessage = 328 + "Storing data locally in the browser without any backup or syncing enabled."; 329 + } 268 330 269 - //////////////////////////////////////////// 270 - // REGISTER 271 - //////////////////////////////////////////// 331 + export default OutputConfig; 272 332 273 - export const CLASS = OutputConfig; 274 - export const NAME = "dtw-output-config"; 333 + //////////////////////////////////////////// 334 + // REGISTER 335 + //////////////////////////////////////////// 336 + 337 + export const CLASS = OutputConfig; 338 + export const NAME = "dtw-output-config"; 275 339 276 - customElements.define(NAME, CLASS); 340 + customElements.define(NAME, CLASS);
+5 -2
src/themes/webamp/facet.css
··· 34 34 box-sizing: border-box; 35 35 display: flex; 36 36 flex-direction: column; 37 - height: 64dvh; 38 - width: min(960px, 86dvw); 37 + 38 + &:not([role="tabpanel"]) { 39 + height: 64dvh; 40 + width: min(960px, 86dvw); 41 + } 39 42 } 40 43 41 44 .window-body {