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.

at v4 145 lines 4.4 kB view raw
1import * as IDB from "idb-keyval"; 2import { xxh32r } from "xxh32/dist/raw.js"; 3 4import { batch, computed, signal, untracked } from "~/common/signal.js"; 5import { OutputTransformer } from "../../base.js"; 6import { defineElement } from "~/common/element.js"; 7 8import { 9 STARTING_SET_DISABLED, 10 STARTING_SET_URIS, 11 TYPE, 12} from "~/common/facets/constants.js"; 13import facets from "~/_data/facets.json" with { 14 type: "json", 15}; 16 17/** 18 * @import {OutputManagerDeputy} from "~/components/output/types.d.ts" 19 */ 20 21const IDB_KEY = 22 "diffuse/transformer/output/refiner/initial-contents/initialized"; 23 24//////////////////////////////////////////// 25// ELEMENT 26//////////////////////////////////////////// 27 28/** 29 * @extends {OutputTransformer} 30 */ 31class InitialContentsTransformer extends OutputTransformer { 32 static NAME = "diffuse/transformer/output/refiner/initial-contents"; 33 34 #flagLoaded = signal(false); 35 #initialized = signal(false); 36 37 constructor() { 38 super(); 39 40 const base = this.base(); 41 42 // Load flag from IDB; gate collection() until resolved to prevent 43 // a flash of defaults for a user who has previously cleared their collection. 44 IDB.get(IDB_KEY).then((v) => { 45 batch(() => { 46 this.#initialized.value = !!v; 47 this.#flagLoaded.value = true; 48 }); 49 }); 50 51 /** @type {OutputManagerDeputy} */ 52 const manager = { 53 facets: { 54 ...base.facets, 55 collection: computed(() => { 56 if (!this.#flagLoaded.get()) return { state: "loading" }; 57 58 const col = base.facets.collection(); 59 if (col.state !== "loaded") return col; 60 61 if (col.data.length > 0) { 62 // Set the flag the first time we observe non-empty data. 63 // Covers data arriving from another device via sync. 64 // untracked read avoids a circular dependency; the write still 65 // notifies subscribers and queues a re-evaluation. 66 if (!untracked(() => this.#initialized.value)) { 67 this.#initialized.value = true; 68 IDB.set(IDB_KEY, true); // fire-and-forget 69 } 70 71 return { state: "loaded", data: col.data }; 72 } 73 74 // Tracked read: keeps the computed subscribed to #initialized 75 // so it re-runs if save() sets it to true with an empty array. 76 if (this.#initialized.get()) { 77 return { state: "loaded", data: col.data }; 78 } 79 80 // Determine starting set 81 const data = facets.flatMap((facet) => { 82 if (STARTING_SET_URIS.includes(facet.url)) { 83 return [{ 84 $type: TYPE, 85 id: uriToRkey("diffuse://" + facet.url), 86 description: facet.desc, 87 enabled: STARTING_SET_DISABLED.includes(facet.url) 88 ? false 89 : true, 90 kind: facet.kind === "prelude" 91 ? /** @type {const} */ ("prelude") 92 : /** @type {const} */ ("interactive"), 93 name: facet.title, 94 tags: facet.tags?.length ? facet.tags : undefined, 95 uri: "diffuse://" + facet.url, 96 }]; 97 } 98 99 return []; 100 }); 101 102 return { state: "loaded", data }; 103 }), 104 105 save: async (newFacets) => { 106 // Set the flag on any explicit save (covers the case where the 107 // user's first action is removing a default from the list). 108 if (!this.#initialized.value) { 109 this.#initialized.value = true; 110 IDB.set(IDB_KEY, true); // fire-and-forget 111 } 112 113 await base.facets.save(newFacets); 114 }, 115 }, 116 117 playlistItems: base.playlistItems, 118 settings: base.settings, 119 tracks: base.tracks, 120 ready: base.ready, 121 }; 122 123 this.facets = manager.facets; 124 this.playlistItems = manager.playlistItems; 125 this.settings = manager.settings; 126 this.tracks = manager.tracks; 127 this.ready = manager.ready; 128 } 129} 130 131export default InitialContentsTransformer; 132 133/** @param {string} uri */ 134function uriToRkey(uri) { 135 return xxh32r(new TextEncoder().encode(uri)).toString(16).padStart(8, "0"); 136} 137 138//////////////////////////////////////////// 139// REGISTER 140//////////////////////////////////////////// 141 142export const CLASS = InitialContentsTransformer; 143export const NAME = "dtor-initial-contents"; 144 145defineElement(NAME, CLASS);