[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

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

Mobile: Use dropdown for sorting

Kasper (Mar 10, 2026, 4:06 PM +0100) 6ec9f626 db7802c5

+62 -55
+62 -55
mobile/src/routes/Library.svelte
··· 171 171 // ) 172 172 173 173 let scroll_container: HTMLElement | undefined = $state() 174 + let sort_menu_open = $state(false) 175 + const sort_options = [ 176 + { key: 'name', label: 'Name', default_desc: false }, 177 + { key: 'artist', label: 'Artist', default_desc: false }, 178 + { key: 'dateAdded', label: 'Date Added', default_desc: true }, 179 + { key: 'playCount', label: 'Plays', default_desc: true }, 180 + ] as const 174 181 175 182 // ── Formatting ───────────────────────────────────────────────────────────── 176 183 ··· 188 195 return tracks_page_options.sort_desc === false ? ' ↑' : ' ↓' 189 196 } 190 197 198 + function sort_label(): string { 199 + const current = sort_options.find((option) => option.key === tracks_page_options.sort_key) 200 + return `${current?.label ?? 'Sort'}${sort_indicator(tracks_page_options.sort_key)}` 201 + } 202 + 191 203 function format_duration(seconds: number): string { 192 204 const s = Math.floor(seconds) 193 205 return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` ··· 209 221 <div 210 222 class="flex h-screen flex-col overflow-hidden bg-white text-sm text-neutral-800 dark:bg-neutral-950 dark:text-neutral-200" 211 223 > 212 - <!-- Header --> 213 224 <header 214 225 class="flex shrink-0 items-center gap-2 border-b border-neutral-200 px-4 py-3 dark:border-neutral-800" 215 226 > ··· 234 245 {/if} 235 246 </div> 236 247 237 - {@render open_button()} 248 + {#if view.kind === 'browser'} 249 + {@render open_button()} 250 + {:else} 251 + <details 252 + aria-label="Sort" 253 + class="relative z-10 shrink-0 cursor-default select-none" 254 + bind:open={sort_menu_open} 255 + > 256 + <summary 257 + class="list-none rounded border border-neutral-300 px-2.5 py-1 text-xs text-neutral-500 transition-colors hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300" 258 + > 259 + {sort_label()} 260 + </summary> 261 + <div 262 + class="absolute right-0 mt-2 w-40 rounded-lg border border-neutral-200 bg-white py-1 text-xs shadow-lg dark:border-neutral-800 dark:bg-neutral-900" 263 + > 264 + {#each sort_options as option} 265 + <button 266 + type="button" 267 + onclick={() => { 268 + toggle_sort(option.key, option.default_desc) 269 + sort_menu_open = false 270 + }} 271 + class="flex w-full items-center justify-between px-3 py-2 text-left text-neutral-700 hover:bg-neutral-100 dark:text-neutral-200 dark:hover:bg-neutral-800 {tracks_page_options.sort_key === 272 + option.key 273 + ? 'font-semibold' 274 + : ''}" 275 + > 276 + <span>{option.label}</span> 277 + <span class="text-neutral-400 dark:text-neutral-600"> 278 + {sort_indicator(option.key)} 279 + </span> 280 + </button> 281 + {/each} 282 + </div> 283 + </details> 284 + {/if} 238 285 </header> 239 286 240 287 {#if error} ··· 364 411 <!-- ── Tracks view ────────────────────────────────────────────────────── --> 365 412 {:else if view.kind === 'tracks'} 366 413 <div class="shrink-0 border-b border-neutral-200 dark:border-neutral-800"> 367 - <div class="no-scrollbar flex items-center gap-1.5 overflow-x-auto px-4 py-2"> 368 - <div class="relative shrink-0"> 414 + <div class="no-scrollbar flex w-full items-center gap-1.5 overflow-x-auto px-4 py-2"> 415 + <div class="relative flex grow"> 369 416 <span 370 - class="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-xs text-neutral-500 select-none" 417 + class="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-xl text-neutral-500 select-none" 371 418 >⌕</span 372 419 > 373 420 <input 374 421 type="search" 375 422 placeholder="Search…" 376 423 bind:value={tracks_page_options.filter_query} 377 - class="w-32 rounded-lg border border-neutral-300 bg-neutral-100 py-1.5 pr-3 pl-7 text-xs text-neutral-800 placeholder-neutral-400 transition-colors outline-none focus:border-neutral-400 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:placeholder-neutral-600 dark:focus:border-neutral-500" 424 + class="w-full rounded-lg border border-neutral-300 bg-neutral-100 py-1.5 pr-3 pl-7 text-xs text-neutral-800 placeholder-neutral-400 transition-colors outline-none focus:border-neutral-400 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:placeholder-neutral-600 dark:focus:border-neutral-500" 378 425 /> 379 426 </div> 380 - <div class="mx-0.5 h-4 w-px shrink-0 bg-neutral-100 dark:bg-neutral-800"></div> 381 - <button 382 - type="button" 383 - onclick={() => toggle_sort('name')} 384 - class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key === 385 - 'name' 386 - ? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100' 387 - : 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}" 388 - > 389 - Name{sort_indicator('name')} 390 - </button> 391 - <button 392 - type="button" 393 - onclick={() => toggle_sort('artist')} 394 - class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key === 395 - 'artist' 396 - ? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100' 397 - : 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}" 398 - > 399 - Artist{sort_indicator('artist')} 400 - </button> 401 - <button 402 - type="button" 403 - onclick={() => toggle_sort('dateAdded', true)} 404 - class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key === 405 - 'dateAdded' 406 - ? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100' 407 - : 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}" 408 - > 409 - Date Added{sort_indicator('dateAdded')} 410 - </button> 411 - <button 412 - type="button" 413 - onclick={() => toggle_sort('playCount', true)} 414 - class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key === 415 - 'playCount' 416 - ? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100' 417 - : 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}" 418 - > 419 - Plays{sort_indicator('playCount')} 420 - </button> 421 - <span 422 - class="ml-auto shrink-0 pl-1 text-xs text-neutral-400 tabular-nums dark:text-neutral-600" 423 - > 427 + <span class="shrink-0 pl-1 text-xs text-neutral-400 tabular-nums dark:text-neutral-600"> 424 428 {tracks_page?.item_ids.length}/{tracks_page?.playlist_length} 425 429 </span> 426 430 </div> 427 431 428 - <div class="no-scrollbar flex items-center gap-1.5 overflow-x-auto px-4 pb-2"> 429 - <!-- <button 432 + <!-- <div class="no-scrollbar flex items-center gap-1.5 overflow-x-auto px-4 pb-2"> 433 + <button 430 434 type="button" 431 435 onclick={() => (tracks_page_options.filter_query = '')} 432 436 class="shrink-0 rounded-full border px-2.5 py-1 text-xs transition-colors {tracks_page_options ··· 436 440 > 437 441 All 438 442 </button> --> 439 - <!-- {#each genres as genre} 443 + <!-- {#each genres as genre} 440 444 <button 441 445 type="button" 442 446 onclick={() => (active_filter = { kind: 'genre', value: genre })} ··· 447 451 > 448 452 {genre} 449 453 </button> 450 - {/each} --> 451 - </div> 454 + {/each} 455 + </div> --> 452 456 </div> 453 457 454 458 <div class="flex-1 overflow-y-auto" bind:this={scroll_container}> ··· 529 533 .no-scrollbar { 530 534 -ms-overflow-style: none; 531 535 scrollbar-width: none; 536 + } 537 + summary::-webkit-details-marker { 538 + display: none; 532 539 } 533 540 </style>