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

Configure Feed

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

feat: filter by "base" facets

+71 -31
+3
src/_components/grid.vto
··· 5 5 <button class="button--border button--tiny button--bg-twist-4 button--tr-twist-4 button--transparent" data-filter="prelude">Features</button> 6 6 <button class="button--border button--tiny button--bg-twist-2 button--tr-twist-2 button--transparent" data-filter="interface">Interfaces</button> 7 7 8 + <button class="button--border button--tiny button--transparent" title="Show the essential default features" data-filter="base">Base</button> 9 + 8 10 <div style="flex: 1"></div> 9 11 10 12 <span class="grid-filter--label">Category</span> ··· 37 39 data-name="{{item.title}}" 38 40 data-category="{{ item.category ?? `` }}" 39 41 data-kind="{{item.kind ?? `interface`}}" 42 + data-tags="{{ item.tags?.join(',') ?? `` }}" 40 43 data-uri="{{ facetUri }}" 41 44 > 42 45 <div
+17 -15
src/_data/facets.json
··· 32 32 { 33 33 "url": "facets/misc/command/index.html", 34 34 "title": "Command Menu", 35 - "category": "Misc", 36 - "featured": true, 37 - "desc": "A command palette for common actions: add the now-playing track to Favourites, select or create playlists, remove playlists, and toggle repeat/shuffle." 35 + "category": "Miscellaneous", 36 + "desc": "A command palette for various actions." 38 37 }, 39 38 { 40 39 "url": "facets/connect/index.html", ··· 80 79 "desc": "Connect to an S3-compatible storage for audio input or user-data storage." 81 80 }, 82 81 { 83 - "url": "facets/data/export-import/index.html", 84 - "title": "Export & Import", 85 - "category": "Data", 86 - "desc": "Export all data as a JSON snapshot, or restore from a previously exported file." 87 - }, 88 - { 89 82 "url": "facets/data/artwork-bundle/index.html", 90 83 "title": "Default Artwork Bundle", 91 84 "kind": "prelude", 92 85 "category": "Data", 86 + "tags": ["base"], 93 87 "desc": "The default setup for track artwork retrieval. Adds support for: embedded audio metadata, Last.fm, and MusicBrainz." 94 88 }, 95 89 { ··· 97 91 "title": "Default Input Bundle", 98 92 "kind": "prelude", 99 93 "category": "Data", 94 + "tags": ["base"], 100 95 "desc": "The default setup for audio input sources. Adds support for: HTTPS, Icecast, the local filesystem, OpenSubsonic, and S3-compatible storage." 101 96 }, 102 97 { ··· 104 99 "title": "Default Metadata Bundle", 105 100 "kind": "prelude", 106 101 "category": "Data", 102 + "tags": ["base"], 107 103 "desc": "The default setup for track metadata lookups. Reads tags and audio stats from audio files." 108 104 }, 109 105 { ··· 111 107 "title": "Default Output Bundle", 112 108 "kind": "prelude", 113 109 "category": "Data", 110 + "tags": ["base"], 114 111 "desc": "The default setup for user-data storage output. Adds support for: AT Protocol and S3-compatible storage. For both of these a custom local-first syncing algorithm is used." 115 112 }, 116 113 { 114 + "url": "facets/data/export-import/index.html", 115 + "title": "Export & Import", 116 + "category": "Data", 117 + "desc": "Export all data as a JSON snapshot, or restore from a previously exported file." 118 + }, 119 + { 117 120 "url": "facets/playback/preload/prelude/index.html", 118 121 "title": "Preload Tracks", 119 122 "kind": "prelude", ··· 125 128 "title": "Process Tracks", 126 129 "kind": "prelude", 127 130 "category": "Data", 128 - "featured": true, 129 131 "desc": "Process all your audio inputs into tracks automatically when opening any interface. Only happens once every 10 minutes, if the processing was completed." 130 132 }, 131 133 { ··· 139 141 "url": "facets/misc/scrobble/index.html", 140 142 "title": "Scrobble", 141 143 "kind": "prelude", 142 - "category": "Misc", 144 + "category": "Miscellaneous", 143 145 "desc": "Enable scrobbling, keep track of what you're listening to. Adds support for these scrobblers: Last.fm, ListenBrainz, Rocksky" 144 146 }, 145 147 { 146 148 "url": "facets/misc/scrobble/last.fm/index.html", 147 149 "title": "Scrobble / Last.fm", 148 - "category": "Misc", 150 + "category": "Miscellaneous", 149 151 "desc": "Connect to Last.fm to setup the Last.fm scrobbler." 150 152 }, 151 153 { 152 154 "url": "facets/misc/scrobble/listenbrainz/index.html", 153 155 "title": "Scrobble / ListenBrainz", 154 - "category": "Misc", 156 + "category": "Miscellaneous", 155 157 "desc": "Connect to ListenBrainz to setup the ListenBrainz scrobbler." 156 158 }, 157 159 { 158 160 "url": "facets/misc/scrobble/rocksky/index.html", 159 161 "title": "Scrobble / Rocksky", 160 - "category": "Misc", 162 + "category": "Miscellaneous", 161 163 "desc": "Connect to Rocksky to setup the Rocksky scrobbler." 162 164 }, 163 165 { ··· 170 172 { 171 173 "url": "facets/misc/split-view/index.html", 172 174 "title": "Split View", 173 - "category": "Misc", 175 + "category": "Miscellaneous", 174 176 "desc": "Arrange multiple facets side-by-side in a resizable split-panel layout." 175 177 }, 176 178 {
+3 -2
src/common/facets/utils.js
··· 8 8 */ 9 9 10 10 /** 11 - * @param {{ description?: string; kind: string | undefined; name: string; uri: string }} _args 11 + * @param {{ description?: string; kind: string | undefined; name: string; tags?: string[]; uri: string }} _args 12 12 * @param {{ fetchHTML: boolean }} options 13 13 */ 14 14 export async function facetFromURI( 15 - { description, kind, name, uri }, 15 + { description, kind, name, tags, uri }, 16 16 { fetchHTML }, 17 17 ) { 18 18 const html = fetchHTML ? await loadURI(uri) : undefined; ··· 31 31 html, 32 32 name, 33 33 kind: kind === "interactive" || kind === "prelude" ? kind : undefined, 34 + tags: tags?.length ? tags : undefined, 34 35 updatedAt: timestamp, 35 36 uri, 36 37 };
+25 -3
src/common/pages/dashboard.js
··· 16 16 const FILTER_STORAGE_KEY = "diffuse/dashboard/filter"; 17 17 const storedFilter = localStorage.getItem(FILTER_STORAGE_KEY); 18 18 const activeFilter = signal( 19 - storedFilter === "prelude" || storedFilter === "interface" 19 + storedFilter === "prelude" || storedFilter === "interface" || 20 + storedFilter === "base" 20 21 ? storedFilter 21 22 : "all", 22 23 ); ··· 52 53 /** */ 53 54 export async function renderList() { 54 55 if (stopMonitor) stopMonitor(); 56 + activeFilter.set((() => { 57 + const stored = localStorage.getItem(FILTER_STORAGE_KEY); 58 + return stored === "prelude" || stored === "interface" || stored === "base" 59 + ? stored 60 + : "all"; 61 + })()); 55 62 56 63 /** @type {HTMLElement | null} */ 57 64 const listEl = document.querySelector("#list"); ··· 93 100 const col = facetsCol.state === "loaded" 94 101 ? [...facetsCol.data] 95 102 .filter((c) => 96 - filter === "all" || 97 - (filter === "prelude" ? c.kind === "prelude" : c.kind !== "prelude") 103 + filter === "base" 104 + ? !!c.tags?.includes("base") 105 + : (filter === "all" || 106 + (filter === "prelude" 107 + ? c.kind === "prelude" 108 + : c.kind !== "prelude")) && 109 + !c.tags?.includes("base") 98 110 ) 99 111 .sort((a, b) => { 100 112 return a.name.toLocaleLowerCase().localeCompare( ··· 135 147 @click="${() => activeFilter.set("interface")}" 136 148 > 137 149 Interfaces 150 + </button> 151 + 152 + <button 153 + class="button--border button--tiny ${filter === "base" 154 + ? "" 155 + : "button--transparent"}" 156 + title="Show the essential default features" 157 + @click="${() => activeFilter.set("base")}" 158 + > 159 + Base 138 160 </button> 139 161 140 162 <span class="divider"></span>
+21 -11
src/common/pages/grid.js
··· 69 69 categoriesEl.appendChild(categoryMenuEl); 70 70 } 71 71 72 + const FILTER_KIND_STORAGE_KEY = "diffuse/dashboard/filter"; 73 + 72 74 let activeKind = "all"; 73 75 let activeCategory = "all"; 74 76 ··· 87 89 categoryLabelEl.textContent = category === "all" ? "All" : category; 88 90 } 89 91 items.forEach((item) => { 90 - const kindMatch = kind === "all" || item.dataset.kind === kind; 91 - const catMatch = category === "all" || item.dataset.category === category; 92 - item.hidden = !(kindMatch && catMatch); 92 + const isBase = (item.dataset.tags ?? "").split(",").includes("base"); 93 + if (kind === "base") { 94 + item.hidden = !isBase; 95 + } else { 96 + const kindMatch = kind === "all" || item.dataset.kind === kind; 97 + const catMatch = category === "all" || item.dataset.category === category; 98 + item.hidden = !(kindMatch && catMatch && !isBase); 99 + } 93 100 }); 94 101 } 95 102 96 103 kindButtons.forEach((b) => { 97 104 b.addEventListener("click", () => { 98 105 activeKind = b.dataset.filter ?? "all"; 99 - const url = new URL(location.href); 100 - if (activeKind === "all") url.searchParams.delete("filter"); 101 - else url.searchParams.set("filter", activeKind); 102 - history.replaceState(null, "", url); 106 + localStorage.setItem(FILTER_KIND_STORAGE_KEY, activeKind); 103 107 applyFilter(activeKind, activeCategory); 104 108 }); 105 109 }); 106 110 107 - const params = new URL(location.href).searchParams; 108 - activeKind = params.get("filter") ?? "all"; 109 - activeCategory = params.get("category") ?? "all"; 111 + const storedKind = localStorage.getItem(FILTER_KIND_STORAGE_KEY); 112 + activeKind = 113 + storedKind === "prelude" || storedKind === "interface" || 114 + storedKind === "base" 115 + ? storedKind 116 + : "all"; 117 + activeCategory = new URL(location.href).searchParams.get("category") ?? "all"; 110 118 applyFilter(activeKind, activeCategory); 111 119 } 112 120 ··· 128 136 const name = li.getAttribute("data-name"); 129 137 const kind = li.getAttribute("data-kind") ?? undefined; 130 138 const description = li.getAttribute("data-description") ?? undefined; 139 + const tagsRaw = li.getAttribute("data-tags"); 140 + const tags = tagsRaw ? tagsRaw.split(",").filter(Boolean) : undefined; 131 141 132 142 if (!uri || !name) return; 133 143 ··· 140 150 if (isActive) { 141 151 out.facets.save(collection.filter((f) => f.uri !== uri)); 142 152 } else { 143 - const facet = await facetFromURI({ description, kind, name, uri }, { 153 + const facet = await facetFromURI({ description, kind, name, tags, uri }, { 144 154 fetchHTML: false, 145 155 }); 146 156 out.facets.save([...collection, facet]);
+1
src/components/transformer/output/refiner/initial-contents/element.js
··· 84 84 ? /** @type {const} */ ("prelude") 85 85 : /** @type {const} */ ("interactive"), 86 86 name: facet.title, 87 + tags: facet.tags?.length ? facet.tags : undefined, 87 88 uri: "diffuse://" + facet.url, 88 89 }]; 89 90 }
+1
src/definitions/output/facet.json
··· 34 34 "description": "A facet is by default interactive, but headless 'prelude' facets may also be created, these run before any main interactive facet is loaded." 35 35 }, 36 36 "name": { "type": "string" }, 37 + "tags": { "type": "array", "items": { "type": "string" } }, 37 38 "updatedAt": { "type": "string", "format": "datetime" }, 38 39 "uri": { 39 40 "type": "string",