forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
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);