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: sorting

+113 -2
+17
src/components/engine/scope/element.js
··· 12 12 13 13 #playlist = signal(/** @type {string | undefined} */ (undefined)); 14 14 #searchTerm = signal(/** @type {string | undefined} */ (undefined)); 15 + #sortBy = signal(/** @type {string[]} */ ([])); 15 16 16 17 playlist = this.#playlist.get; 17 18 searchTerm = this.#searchTerm.get; 19 + sortBy = this.#sortBy.get; 18 20 19 21 // LIFECYCLE 20 22 ··· 27 29 const actions = this.broadcast(this.identifier, { 28 30 setPlaylist: { strategy: "replicate", fn: this.setPlaylist }, 29 31 setSearchTerm: { strategy: "replicate", fn: this.setSearchTerm }, 32 + setSortBy: { strategy: "replicate", fn: this.setSortBy }, 30 33 }); 31 34 32 35 if (actions) { 33 36 this.setPlaylist = actions.setPlaylist; 34 37 this.setSearchTerm = actions.setSearchTerm; 38 + this.setSortBy = actions.setSortBy; 35 39 } 36 40 } 37 41 ··· 46 50 localStorage.getItem(`${storagePrefix}/playlistId`) ?? undefined; 47 51 this.#searchTerm.value = 48 52 localStorage.getItem(`${storagePrefix}/searchTerm`) ?? undefined; 53 + this.#sortBy.value = 54 + JSON.parse(localStorage.getItem(`${storagePrefix}/sortBy`) ?? "[]"); 49 55 50 56 // Effects 51 57 this.effect(() => { ··· 63 69 if (val) localStorage.setItem(key, val); 64 70 else localStorage.removeItem(key); 65 71 }); 72 + 73 + this.effect(() => { 74 + const key = `${storagePrefix}/sortBy`; 75 + const val = this.#sortBy.value; 76 + 77 + if (val.length) localStorage.setItem(key, JSON.stringify(val)); 78 + else localStorage.removeItem(key); 79 + }); 66 80 } 67 81 68 82 // ACTIONS ··· 72 86 73 87 /** @param {string | undefined} val */ 74 88 setSearchTerm = async (val) => this.#searchTerm.value = val; 89 + 90 + /** @param {string[]} val */ 91 + setSortBy = async (val) => this.#sortBy.value = val; 75 92 } 76 93 77 94 export default ScopeEngine;
+29 -2
src/components/orchestrator/scoped-tracks/element.js
··· 183 183 } 184 184 }); 185 185 186 - // Watch `#tracksSearch` + Playlist 186 + // Watch `#tracksSearch` + Playlist + Sort 187 187 this.effect(async () => { 188 188 const tracks = this.#tracksSearch.value; 189 189 const playlistItems = this.#selectedPlaylistItems(); 190 + const sortBy = this.#scope.value?.sortBy(); 190 191 191 192 if ((await this.isLeader()) === false) return; 192 193 193 - const final = playlistItems?.length 194 + let final = playlistItems?.length 194 195 ? filterByPlaylist(tracks, playlistItems) 195 196 : tracks; 197 + 198 + if (sortBy?.length) { 199 + final = [...final].sort((a, b) => { 200 + for (const field of sortBy) { 201 + let aVal = /** @type {any} */ (a); 202 + let bVal = /** @type {any} */ (b); 203 + 204 + for (const key of field.split(".")) { 205 + aVal = aVal?.[key]; 206 + bVal = bVal?.[key]; 207 + } 208 + 209 + if (aVal == null && bVal == null) continue; 210 + if (aVal == null) return 1; 211 + if (bVal == null) return -1; 212 + 213 + const cmp = typeof aVal === "string" && typeof bVal === "string" 214 + ? aVal.localeCompare(bVal) 215 + : aVal < bVal ? -1 : aVal > bVal ? 1 : 0; 216 + 217 + if (cmp !== 0) return cmp; 218 + } 219 + 220 + return 0; 221 + }); 222 + } 196 223 197 224 this.#tracksFinal.set(final); 198 225 });
+14
src/facets/tools/auto-queue.html
··· 23 23 <input id="search" type="search" placeholder="Search" /> 24 24 </label> 25 25 </div> 26 + <div class="row"> 27 + <label class="with-icon" for="sort-by"> 28 + <i class="ph-bold ph-arrows-down-up"></i> 29 + <select id="sort-by"> 30 + <option value="[]">Default order</option> 31 + <option value='["createdAt"]'>Added to collection</option> 32 + <option value='["tags.title"]'>Title</option> 33 + <option value='["tags.album","tags.disc.no","tags.track.no"]'>Album</option> 34 + <option value='["tags.artist","tags.album","tags.disc.no","tags.track.no"]'>Artist</option> 35 + <option value='["tags.year"]'>Year</option> 36 + <option value='["tags.date"]'>Date</option> 37 + </select> 38 + </label> 39 + </div> 26 40 </div> 27 41 </main> 28 42
+17
src/facets/tools/auto-queue.inline.js
··· 21 21 /** @type {HTMLInputElement} */ (document.querySelector("#search")); 22 22 const playlistSelect = 23 23 /** @type {HTMLSelectElement} */ (document.querySelector("#playlist")); 24 + const sortBySelect = 25 + /** @type {HTMLSelectElement} */ (document.querySelector("#sort-by")); 24 26 25 27 // Repeat & Shuffle state 26 28 effect(() => { ··· 96 98 playlistSelect.value.length ? playlistSelect.value : undefined, 97 99 ); 98 100 }; 101 + 102 + // Sort by state 103 + effect(() => { 104 + const current = JSON.stringify(scope.sortBy()); 105 + for (const option of sortBySelect.options) { 106 + if (JSON.stringify(JSON.parse(option.value)) === current) { 107 + sortBySelect.value = option.value; 108 + break; 109 + } 110 + } 111 + }); 112 + 113 + sortBySelect.onchange = () => { 114 + scope.setSortBy(JSON.parse(sortBySelect.value)); 115 + };
+36
src/themes/webamp/browser/element.js
··· 16 16 const ROW_HEIGHT = 14; 17 17 const OVERSCAN = 20; 18 18 19 + const SORT_OPTIONS = [ 20 + { label: "Default order", value: [] }, 21 + { label: "Added to collection", value: ["createdAt"] }, 22 + { label: "Title", value: ["tags.title"] }, 23 + { label: "Album", value: ["tags.album", "tags.disc.no", "tags.track.no"] }, 24 + { label: "Artist", value: ["tags.artist", "tags.album", "tags.disc.no", "tags.track.no"] }, 25 + { label: "Year", value: ["tags.year"] }, 26 + { label: "Date", value: ["tags.date"] }, 27 + ]; 28 + 19 29 class Browser extends DiffuseElement { 20 30 constructor() { 21 31 super(); ··· 125 135 /** @type {HTMLSelectElement} */ (select).value = playlist ?? ""; 126 136 } 127 137 }); 138 + 139 + this.effect(() => { 140 + const sortBy = this.$scope.value?.sortBy(); 141 + const select = this.root().querySelector("#sort-by-select"); 142 + 143 + if (select) { 144 + /** @type {HTMLSelectElement} */ (select).value = JSON.stringify(sortBy ?? []); 145 + } 146 + }); 128 147 } 129 148 130 149 /** ··· 220 239 this.$scope.value?.setPlaylist(value === "" ? undefined : value); 221 240 }; 222 241 242 + /** 243 + * @param {Event} event 244 + */ 245 + setSortBy = (event) => { 246 + const value = /** @type {HTMLSelectElement} */ (event.currentTarget).value; 247 + 248 + this.$scope.value?.setSortBy(JSON.parse(value)); 249 + }; 250 + 223 251 // RENDER 224 252 225 253 /** ··· 231 259 const tracks = this.$provider.value?.tracks() ?? []; 232 260 const playlist = this.$scope.value?.playlist(); 233 261 const searchTerm = this.$scope.value?.searchTerm() ?? ""; 262 + const sortByJson = JSON.stringify(this.$scope.value?.sortBy() ?? []); 234 263 235 264 // Virtual list 236 265 const totalTracks = tracks.length; ··· 350 379 </optgroup> 351 380 ` 352 381 )} 382 + </select> 383 + <label for="sort-by-select">Sort:</label> 384 + <select id="sort-by-select" @change="${this.setSortBy}"> 385 + ${SORT_OPTIONS.map((opt) => { 386 + const json = JSON.stringify(opt.value); 387 + return html`<option value="${json}" ?selected="${sortByJson === json}">${opt.label}</option>`; 388 + })} 353 389 </select> 354 390 </search> 355 391