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: remove all things themes from outputs

+9 -277
+3 -3
docs/ARCHITECTURE.md
··· 27 27 28 28 ## Other directories 29 29 30 - - `src/common`: Common Javascript code shared by various components and/or themes. 31 - - `src/styles`: Common CSS shared by themes, the index page or facets. 32 - - `src/favicons`, `src/fonts`, `src/images` are binary assets for themes and the index page (`src/index.vto`) 30 + - `src/common`: Common Javascript code shared by various components and/or pages. 31 + - `src/styles`: Common CSS shared by pages or facets. 32 + - `src/favicons`, `src/fonts`, `src/images` are binary assets for facets and pages. 33 33 - `src/_components` and `src/_includes` are templates used in `.vto` templates.
-1
src/build.vto
··· 107 107 </p> 108 108 <ul> 109 109 <li><span>Most elements are configured in broadcast mode so they communicate across tabs. There are a few exceptions such as inputs, where we prefer parallelisation.</span></li> 110 - <li><span>You can use facets in combination with themes by adding the elements used in the theme to a group and then passing in the group name as a URL query parameter (eg. <code>group=facets</code>)</span></li> 111 110 </ul> 112 111 </section> 113 112 </div>
+1 -47
src/components/configurator/output/element.js
··· 3 3 4 4 /** 5 5 * @import {DiffuseElement} from "~/common/element.js" 6 - * @import {Facet, PlaylistItem, Theme, Track} from "~/definitions/types.d.ts" 6 + * @import {Facet, PlaylistItem, Track} from "~/definitions/types.d.ts" 7 7 * @import {OutputManagerDeputy, OutputElement} from "~/components/output/types.d.ts" 8 8 * 9 9 * @import {OutputConfiguratorElement} from "./types.d.ts" ··· 94 94 this.#memory.playlistItems.value = newPlaylistItems; 95 95 }, 96 96 }, 97 - themes: { 98 - collection: computed(() => { 99 - const out = this.#selected.value; 100 - if (out) return out.themes.collection(); 101 - 102 - const def = this.#defaultOutput.value; 103 - if (def) return def.themes.collection(); 104 - if (this.hasDefault()) return { state: "loading" }; 105 - 106 - return this.#setupFinished.value 107 - ? { state: "loaded", data: this.#memory.themes.value } 108 - : { state: "loading" }; 109 - }), 110 - reload: () => { 111 - const def = this.#defaultOutput.value; 112 - if (def) def.themes.reload(); 113 - 114 - const out = this.#selected.value; 115 - if (out) return out.themes.reload(); 116 - 117 - return Promise.resolve(); 118 - }, 119 - save: async (newThemes) => { 120 - const out = this.#selected.value; 121 - if (out) return await out.themes.save(newThemes); 122 - 123 - const def = this.#defaultOutput.value; 124 - if (def) return await def.themes.save(newThemes); 125 - 126 - this.#memory.themes.value = newThemes; 127 - }, 128 - }, 129 97 tracks: { 130 98 collection: computed(() => { 131 99 const out = this.#selected.value; ··· 174 142 // Assign manager properties to class 175 143 this.facets = manager.facets; 176 144 this.playlistItems = manager.playlistItems; 177 - this.themes = manager.themes; 178 145 this.tracks = manager.tracks; 179 146 this.ready = manager.ready; 180 147 ··· 212 179 const out = this.#selected.value; 213 180 if (!out) return; 214 181 215 - const col = out.themes.collection(); 216 - if (col.state !== "loaded") return; 217 - 218 - const def = this.#defaultOutput.value; 219 - if (def) def.themes.save(col.data); 220 - else this.#memory.themes.set(col.data); 221 - }); 222 - 223 - this.effect(() => { 224 - const out = this.#selected.value; 225 - if (!out) return; 226 - 227 182 const col = out.tracks.collection(); 228 183 if (col.state !== "loaded") return; 229 184 ··· 244 199 #memory = { 245 200 facets: signal(/** @type {Facet[]} */ ([])), 246 201 playlistItems: signal(/** @type {PlaylistItem[]} */ ([])), 247 - themes: signal(/** @type {Theme[]} */ ([])), 248 202 tracks: signal(/** @type {Track[]} */ ([])), 249 203 }; 250 204
-4
src/components/orchestrator/output/element.js
··· 65 65 return this.output.playlistItems; 66 66 } 67 67 68 - get themes() { 69 - return this.output.themes; 70 - } 71 - 72 68 get tracks() { 73 69 return this.output.tracks; 74 70 }
-1
src/components/orchestrator/path-collections/element.js
··· 37 37 ); 38 38 }, 39 39 }; 40 - this.themes = base.themes; 41 40 this.tracks = base.tracks; 42 41 43 42 this.ready = base.ready;
-6
src/components/output/bytes/s3/element.js
··· 45 45 get: () => this.#get("playlistItems"), 46 46 put: (data) => this.#put("playlistItems", data), 47 47 }, 48 - themes: { 49 - empty: () => undefined, 50 - get: () => this.#get("themes"), 51 - put: (data) => this.#put("themes", data), 52 - }, 53 48 tracks: { 54 49 empty: () => undefined, 55 50 get: () => this.#get("tracks"), ··· 59 54 60 55 this.facets = this.#manager.facets; 61 56 this.playlistItems = this.#manager.playlistItems; 62 - this.themes = this.#manager.themes; 63 57 this.tracks = this.#manager.tracks; 64 58 } 65 59
+2 -40
src/components/output/common.js
··· 3 3 import { strictEquality } from "~/common/compare.js"; 4 4 5 5 /** 6 - * @import {Facet, PlaylistItem, Theme, Track} from "~/definitions/types.d.ts" 6 + * @import {Facet, PlaylistItem, Track} from "~/definitions/types.d.ts" 7 7 * @import {SignalWriter} from "~/common/signal.d.ts"; 8 8 * @import {OutputManager, OutputManagerProperties} from "./types.d.ts" 9 9 */ ··· 33 33 34 34 const ogFacetsSave = manager.facets.save.bind(this); 35 35 const ogPlaylistItemsSave = manager.playlistItems.save.bind(this); 36 - const ogThemesSave = manager.themes.save.bind(this); 37 36 const ogTracksSave = manager.tracks.save.bind(this); 38 37 39 38 const actions = this.broadcast(this.identifier, { ··· 48 47 set: manager.signals.playlistItems.set, 49 48 }), 50 49 }, 51 - saveThemes: { 52 - strategy: "replicate", 53 - fn: fn({ save: ogThemesSave, set: manager.signals.themes.set }), 54 - }, 55 50 saveTracks: { 56 51 strategy: "replicate", 57 52 fn: fn({ save: ogTracksSave, set: manager.signals.tracks.set }), ··· 61 56 if (actions) { 62 57 manager.facets.save = actions.saveFacets; 63 58 manager.playlistItems.save = actions.savePlaylistItems; 64 - manager.themes.save = actions.saveThemes; 65 59 manager.tracks.save = actions.saveTracks; 66 60 } 67 61 } ··· 73 67 * @returns {OutputManager<Encoding>} 74 68 */ 75 69 export function outputManager( 76 - { init, facets, playlistItems, themes, tracks }, 70 + { init, facets, playlistItems, tracks }, 77 71 ) { 78 72 const c = signal( 79 73 /** @type {Encoding extends null ? Facet[] : Encoding} */ (facets ··· 93 87 { compare: strictEquality }, 94 88 ); 95 89 96 - const th = signal( 97 - /** @type {Encoding extends null ? Theme[] : Encoding} */ (themes.empty()), 98 - ); 99 - const ths = signal( 100 - /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"), 101 - { compare: strictEquality }, 102 - ); 103 - 104 90 const t = signal( 105 91 /** @type {Encoding extends null ? Track[] : Encoding} */ (tracks.empty()), 106 92 ); ··· 123 109 pls.value = "loaded"; 124 110 } 125 111 126 - async function loadThemes() { 127 - if (init && (await init()) === false) return; 128 - ths.value = "loading"; 129 - th.value = await themes.get(); 130 - ths.value = "loaded"; 131 - } 132 - 133 112 async function loadTracks() { 134 113 if (init && (await init()) === false) return; 135 114 ts.value = "loading"; ··· 170 149 await playlistItems.put(newPlaylistItems); 171 150 }, 172 151 }, 173 - themes: { 174 - collection: computed(() => { 175 - if (untracked(() => ths.value === "sleeping")) loadThemes(); 176 - return ths.value === "loaded" 177 - ? { state: "loaded", data: th.value } 178 - : { state: "loading" }; 179 - }), 180 - reload: loadThemes, 181 - save: async (newThemes) => { 182 - batch(() => { 183 - if (untracked(() => ths.value === "sleeping")) ths.value = "loaded"; 184 - th.value = newThemes; 185 - }); 186 - await themes.put(newThemes); 187 - }, 188 - }, 189 152 tracks: { 190 153 collection: computed(() => { 191 154 if (untracked(() => ts.value === "sleeping")) loadTracks(); ··· 205 168 signals: { 206 169 facets: c, 207 170 playlistItems: pl, 208 - themes: th, 209 171 tracks: t, 210 172 }, 211 173 };
-6
src/components/output/polymorphic/indexed-db/element.js
··· 37 37 get: () => this.#get("playlistItems"), 38 38 put: (data) => this.#put("playlistItems", data), 39 39 }, 40 - themes: { 41 - empty: () => undefined, 42 - get: () => this.#get("themes"), 43 - put: (data) => this.#put("themes", data), 44 - }, 45 40 tracks: { 46 41 empty: () => undefined, 47 42 get: () => this.#get("tracks"), ··· 51 46 52 47 this.facets = this.#manager.facets; 53 48 this.playlistItems = this.#manager.playlistItems; 54 - this.themes = this.#manager.themes; 55 49 this.tracks = this.#manager.tracks; 56 50 57 51 this.ready = () => true;
-8
src/components/output/raw/atproto/element.js
··· 33 33 const WATCHED_COLLECTIONS = new Set([ 34 34 "sh.diffuse.output.facet", 35 35 "sh.diffuse.output.playlistItem", 36 - "sh.diffuse.output.theme", 37 36 "sh.diffuse.output.trackBundle", 38 37 ]); 39 38 ··· 76 75 get: () => this.listRecords("sh.diffuse.output.playlistItem"), 77 76 put: (data) => this.#putRecords("sh.diffuse.output.playlistItem", data), 78 77 }, 79 - themes: { 80 - empty: () => [], 81 - get: () => this.listRecords("sh.diffuse.output.theme"), 82 - put: (data) => this.#putRecords("sh.diffuse.output.theme", data), 83 - }, 84 78 tracks: { 85 79 empty: () => [], 86 80 get: async () => { ··· 119 113 120 114 this.facets = this.#manager.facets; 121 115 this.playlistItems = this.#manager.playlistItems; 122 - this.themes = this.#manager.themes; 123 116 this.tracks = this.#manager.tracks; 124 117 } 125 118 ··· 330 323 if (touched.has("sh.diffuse.output.playlistItem")) { 331 324 this.#manager.playlistItems.reload(); 332 325 } 333 - if (touched.has("sh.diffuse.output.theme")) this.#manager.themes.reload(); 334 326 if (touched.has("sh.diffuse.output.trackBundle")) { 335 327 this.#manager.tracks.reload(); 336 328 }
-19
src/components/output/types.d.ts
··· 3 3 import type { 4 4 Facet, 5 5 PlaylistItem, 6 - Theme, 7 6 Track, 8 7 } from "~/definitions/types.d.ts"; 9 8 ··· 39 38 signals: { 40 39 facets: Signal<Encoding extends null ? Facet[] : Encoding>; 41 40 playlistItems: Signal<Encoding extends null ? PlaylistItem[] : Encoding>; 42 - themes: Signal<Encoding extends null ? Theme[] : Encoding>; 43 41 tracks: Signal<Encoding extends null ? Track[] : Encoding>; 44 42 }; 45 - themes: { 46 - collection: SignalReader< 47 - | { state: "loading" } 48 - | { state: "loaded"; data: Encoding extends null ? Theme[] : Encoding } 49 - >; 50 - reload: () => Promise<void>; 51 - save: ( 52 - themes: Encoding extends null ? Theme[] : Encoding, 53 - ) => Promise<void>; 54 - }; 55 43 tracks: { 56 44 collection: SignalReader< 57 45 | { state: "loading" } ··· 76 64 get(): Promise<Encoding extends null ? PlaylistItem[] : Encoding>; 77 65 put( 78 66 playlistItems: Encoding extends null ? PlaylistItem[] : Encoding, 79 - ): Promise<void>; 80 - }; 81 - themes: { 82 - empty(): Encoding extends null ? Theme[] : Encoding; 83 - get(): Promise<Encoding extends null ? Theme[] : Encoding>; 84 - put( 85 - themes: Encoding extends null ? Theme[] : Encoding, 86 67 ): Promise<void>; 87 68 }; 88 69 tracks: {
-14
src/components/transformer/output/base.js
··· 70 70 await this.output.signal()?.playlistItems.save(newPlaylistItems); 71 71 }, 72 72 }, 73 - themes: { 74 - collection: computed(() => { 75 - return this.output.signal()?.themes?.collection() ?? 76 - { state: "loading" }; 77 - }), 78 - reload: () => { 79 - return this.output.signal()?.themes?.reload() ?? 80 - Promise.resolve(); 81 - }, 82 - save: async (newThemes) => { 83 - await this.output.whenDefined; 84 - await this.output.signal()?.themes.save(newThemes); 85 - }, 86 - }, 87 73 tracks: { 88 74 collection: computed(() => { 89 75 return this.output.signal()?.tracks?.collection() ??
+1 -8
src/components/transformer/output/bytes/automerge/constants.js
··· 2 2 import { base64 } from "iso-base/rfc4648"; 3 3 4 4 /** 5 - * @import { FacetsDocument, PlaylistItemsDocument, ThemesDocument, TracksDocument } from "./types.d.ts"; 5 + * @import { FacetsDocument, PlaylistItemsDocument, TracksDocument } from "./types.d.ts"; 6 6 */ 7 7 8 8 /** @type {Automerge.Doc<FacetsDocument>} */ ··· 16 16 export const INITIAL_PLAYLIST_ITEMS_DOCUMENT = Automerge.load( 17 17 base64.decode( 18 18 "hW9Kg5IPZcsAeAEQIyp0LRYp0l9bpZKWJXTPlgGtUD/lrIatFjiIwoUdtJhh/sBQFIcpPppxduoIp1ArXwYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf8eTqcwGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA", 19 - ), 20 - ); 21 - 22 - /** @type {Automerge.Doc<ThemesDocument>} */ 23 - export const INITIAL_THEMES_DOCUMENT = Automerge.load( 24 - base64.decode( 25 - "hW9Kgw5i4LcAeAEQzljXzJAwgqwMkIT3CseCywF4jHbKg9Q2XqVU26bSDj0GtjkQq1HyriZedXU+vUt5wAYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf8iTqcwGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA", 26 19 ), 27 20 ); 28 21
-26
src/components/transformer/output/bytes/automerge/element.js
··· 13 13 import { 14 14 INITIAL_FACETS_DOCUMENT, 15 15 INITIAL_PLAYLIST_ITEMS_DOCUMENT, 16 - INITIAL_THEMES_DOCUMENT, 17 16 INITIAL_TRACKS_DOCUMENT, 18 17 } from "./constants.js"; 19 18 ··· 104 103 INITIAL_PLAYLIST_ITEMS_DOCUMENT, 105 104 ); 106 105 107 - const themes = state( 108 - computed(() => local()?.themes?.collection() ?? { state: "loading" }), 109 - remote.themes.collection, 110 - INITIAL_THEMES_DOCUMENT, 111 - ); 112 - 113 106 const tracks = state( 114 107 computed(() => local()?.tracks?.collection() ?? { state: "loading" }), 115 108 remote.tracks.collection, ··· 131 124 computed(() => playlistItems().doc), 132 125 ); 133 126 134 - this.themes = automergeEntry( 135 - computed(() => local()?.themes), 136 - remote.themes, 137 - computed(() => themes().doc), 138 - { 139 - stripUndefined: true, 140 - }, 141 - ); 142 - 143 127 this.tracks = automergeEntry( 144 128 computed(() => local()?.tracks), 145 129 remote.tracks, ··· 170 154 const bytes = Automerge.save(s.doc); 171 155 if (l && s.local) l.playlistItems.save(bytes); 172 156 if (s.remote) remote.playlistItems.save(bytes); 173 - } 174 - }); 175 - 176 - this.effect(() => { 177 - if (!themes().remoteLoaded) return; 178 - const s = themes(); 179 - if (s.diverged) { 180 - const bytes = Automerge.save(s.doc); 181 - if (l && s.local) l.themes.save(bytes); 182 - if (s.remote) remote.themes.save(bytes); 183 157 } 184 158 }); 185 159
-2
src/components/transformer/output/bytes/automerge/types.d.ts
··· 1 1 import type { 2 2 Facet, 3 3 PlaylistItem, 4 - Theme, 5 4 Track, 6 5 } from "~/definitions/types.d.ts"; 7 6 8 7 export type FacetsDocument = { collection: Facet[] }; 9 8 export type PlaylistItemsDocument = { collection: PlaylistItem[] }; 10 - export type ThemesDocument = { collection: Theme[] }; 11 9 export type TracksDocument = { collection: Track[] };
-16
src/components/transformer/output/bytes/dasl-sync/element.js
··· 158 158 }, 159 159 ); 160 160 161 - const themes = state( 162 - "themes", 163 - computed(() => local()?.themes.collection() ?? { state: "loading" }), 164 - remote.themes.collection, 165 - { 166 - saveLocal: async (v) => local()?.themes.save(v), 167 - saveRemote: remote.themes.save, 168 - }, 169 - ); 170 - 171 161 const tracks = state( 172 162 "tracks", 173 163 computed(() => local()?.tracks.collection() ?? { state: "loading" }), ··· 189 179 { save: async (v) => local()?.playlistItems.save(v) }, 190 180 remote.playlistItems, 191 181 playlistItems, 192 - ); 193 - 194 - this.themes = this.managerProp( 195 - { save: async (v) => local()?.themes.save(v) }, 196 - remote.themes, 197 - themes, 198 182 ); 199 183 200 184 this.tracks = this.managerProp(
+1 -18
src/components/transformer/output/bytes/json/element.js
··· 3 3 4 4 /** 5 5 * @import { OutputManagerDeputy } from "~/components/output/types.d.ts" 6 - * @import { Facet, PlaylistItem, Theme, Track } from "~/definitions/types.d.ts" 6 + * @import { Facet, PlaylistItem, Track } from "~/definitions/types.d.ts" 7 7 */ 8 8 9 9 /** ··· 49 49 await base.playlistItems.save(bytes); 50 50 }, 51 51 }, 52 - themes: { 53 - ...base.themes, 54 - collection: computed(() => { 55 - const col = base.themes.collection(); 56 - if (col.state !== "loaded") return col; 57 - /** @type {Theme[]} */ 58 - const data = parseArray(col.data); 59 - return { state: "loaded", data }; 60 - }), 61 - save: async (newThemes) => { 62 - const json = JSON.stringify(newThemes); 63 - const encoder = new TextEncoder(); 64 - const bytes = encoder.encode(json); 65 - await base.themes.save(bytes); 66 - }, 67 - }, 68 52 tracks: { 69 53 ...base.tracks, 70 54 collection: computed(() => { ··· 89 73 // Assign manager properties to class 90 74 this.facets = manager.facets; 91 75 this.playlistItems = manager.playlistItems; 92 - this.themes = manager.themes; 93 76 this.tracks = manager.tracks; 94 77 this.ready = manager.ready; 95 78 }
-1
src/components/transformer/output/raw/atproto-sync/element.js
··· 15 15 const COLLECTIONS = /** @type {const} */ ([ 16 16 "facets", 17 17 "playlistItems", 18 - "themes", 19 18 "tracks", 20 19 ]); 21 20
-9
src/components/transformer/output/refiner/default/element.js
··· 74 74 await base.playlistItems.save(filtered); 75 75 }, 76 76 }, 77 - themes: { 78 - ...base.themes, 79 - collection: computed(() => { 80 - const col = base.themes.collection(); 81 - if (col.state !== "loaded") return col; 82 - return { state: "loaded", data: col.data }; 83 - }), 84 - }, 85 77 tracks: { 86 78 ...base.tracks, 87 79 collection: computed(() => { ··· 119 111 // Assign manager properties to class 120 112 this.facets = manager.facets; 121 113 this.playlistItems = manager.playlistItems; 122 - this.themes = manager.themes; 123 114 this.tracks = manager.tracks; 124 115 this.ready = manager.ready; 125 116 }
-2
src/components/transformer/output/refiner/initial-contents/element.js
··· 81 81 }, 82 82 83 83 playlistItems: base.playlistItems, 84 - themes: base.themes, 85 84 tracks: base.tracks, 86 85 ready: base.ready, 87 86 }; 88 87 89 88 this.facets = manager.facets; 90 89 this.playlistItems = manager.playlistItems; 91 - this.themes = manager.themes; 92 90 this.tracks = manager.tracks; 93 91 this.ready = manager.ready; 94 92 }
-1
src/components/transformer/output/refiner/track-uri-passkey/element.js
··· 45 45 46 46 this.facets = base.facets; 47 47 this.playlistItems = base.playlistItems; 48 - this.themes = base.themes; 49 48 this.ready = this.#keyReady.get; 50 49 51 50 // Tracks
-16
src/components/transformer/output/string/json/element.js
··· 46 46 await base.playlistItems.save(json); 47 47 }, 48 48 }, 49 - themes: { 50 - ...base.themes, 51 - collection: computed(() => { 52 - const col = base.themes.collection(); 53 - if (col.state !== "loaded") return col; 54 - return { 55 - state: "loaded", 56 - data: typeof col.data === "string" ? parseArray(col.data) : [], 57 - }; 58 - }), 59 - save: async (newThemes) => { 60 - const json = JSON.stringify(newThemes); 61 - await base.themes.save(json); 62 - }, 63 - }, 64 49 tracks: { 65 50 ...base.tracks, 66 51 collection: computed(() => { ··· 84 69 // Assign manager properties to class 85 70 this.facets = manager.facets; 86 71 this.playlistItems = manager.playlistItems; 87 - this.themes = manager.themes; 88 72 this.tracks = manager.tracks; 89 73 this.ready = manager.ready; 90 74 }
+1 -1
src/elements.vto
··· 266 266 title: "Orchestrators", 267 267 items: orchestrators, 268 268 content: ` 269 - These too are element compositions. However, unlike themes, these are purely logical. Mostly exist in order to construct sensible defaults to use across themes and other compositions. 269 + These are element compositions, logic only. Mostly exist in order to construct sensible defaults. 270 270 ` 271 271 }) }} 272 272
-5
src/facets/data/export-import/index.html
··· 33 33 <span class="with-icon"><i class="ph-bold ph-squares-four"></i> Import facets</span> 34 34 </button> 35 35 </div> 36 - <div class="row"> 37 - <button id="import-themes" disabled> 38 - <span class="with-icon"><i class="ph-bold ph-palette"></i> Import themes</span> 39 - </button> 40 - </div> 41 36 <div id="status" class="status" hidden></div> 42 37 </div> 43 38 </main>
-23
src/facets/data/export-import/index.inline.js
··· 18 18 )); 19 19 const importFacetsBtn = 20 20 /** @type {HTMLButtonElement} */ (document.querySelector("#import-facets")); 21 - const importThemesBtn = 22 - /** @type {HTMLButtonElement} */ (document.querySelector("#import-themes")); 23 21 const statusEl = /** @type {HTMLElement} */ (document.querySelector("#status")); 24 22 25 23 /** @type {Record<string, any> | null} */ ··· 45 43 exportBtn.onclick = async () => { 46 44 const facets = await Output.data(output.facets); 47 45 const playlistItems = await Output.data(output.playlistItems); 48 - const themes = await Output.data(output.themes); 49 46 const tracks = await Output.data(output.tracks); 50 47 51 48 const data = { 52 49 exportedAt: new Date().toISOString(), 53 50 facets, 54 51 playlistItems, 55 - themes, 56 52 tracks, 57 53 }; 58 54 ··· 80 76 importTracksBtn.disabled = true; 81 77 importPlaylistItemsBtn.disabled = true; 82 78 importFacetsBtn.disabled = true; 83 - importThemesBtn.disabled = true; 84 79 85 80 if (!file) return; 86 81 ··· 106 101 if (Array.isArray(json?.facets) && json.facets.length > 0) { 107 102 importFacetsBtn.disabled = false; 108 103 } 109 - 110 - if (Array.isArray(json?.themes) && json.themes.length > 0) { 111 - importThemesBtn.disabled = false; 112 - } 113 104 }; 114 105 115 106 // Import tracks ··· 158 149 } 159 150 }; 160 151 161 - // Import themes 162 - importThemesBtn.onclick = async () => { 163 - /** @type {any[]} */ 164 - const themes = json?.themes; 165 - if (!Array.isArray(themes) || themes.length === 0) return; 166 - 167 - try { 168 - await output.themes.save(themes); 169 - showStatus(`Imported ${themes.length} theme(s).`, "success"); 170 - } catch (err) { 171 - console.error("Import failed:", err); 172 - showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 173 - } 174 - };