alpha
Login
or
Join now
kasper.space
/
ferrum
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
[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
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
Mobile: Use dropdown for sorting
author
Kasper
date
4 months ago
(Mar 10, 2026, 4:06 PM +0100)
commit
6ec9f626
6ec9f626fd414e96f2a0fd44a1c5ee35b67e6888
parent
db7802c5
db7802c55f27b6dcec524862caa7f7bddc5d1f88
+62
-55
1 changed file
Expand all
Collapse all
Unified
Split
mobile
src
routes
Library.svelte
+62
-55
mobile/src/routes/Library.svelte
View file
Reviewed
···
171
171
// )
172
172
173
173
let scroll_container: HTMLElement | undefined = $state()
174
174
+
let sort_menu_open = $state(false)
175
175
+
const sort_options = [
176
176
+
{ key: 'name', label: 'Name', default_desc: false },
177
177
+
{ key: 'artist', label: 'Artist', default_desc: false },
178
178
+
{ key: 'dateAdded', label: 'Date Added', default_desc: true },
179
179
+
{ key: 'playCount', label: 'Plays', default_desc: true },
180
180
+
] as const
174
181
175
182
// ── Formatting ─────────────────────────────────────────────────────────────
176
183
···
188
195
return tracks_page_options.sort_desc === false ? ' ↑' : ' ↓'
189
196
}
190
197
198
198
+
function sort_label(): string {
199
199
+
const current = sort_options.find((option) => option.key === tracks_page_options.sort_key)
200
200
+
return `${current?.label ?? 'Sort'}${sort_indicator(tracks_page_options.sort_key)}`
201
201
+
}
202
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
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
237
-
{@render open_button()}
248
248
+
{#if view.kind === 'browser'}
249
249
+
{@render open_button()}
250
250
+
{:else}
251
251
+
<details
252
252
+
aria-label="Sort"
253
253
+
class="relative z-10 shrink-0 cursor-default select-none"
254
254
+
bind:open={sort_menu_open}
255
255
+
>
256
256
+
<summary
257
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
258
+
>
259
259
+
{sort_label()}
260
260
+
</summary>
261
261
+
<div
262
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
263
+
>
264
264
+
{#each sort_options as option}
265
265
+
<button
266
266
+
type="button"
267
267
+
onclick={() => {
268
268
+
toggle_sort(option.key, option.default_desc)
269
269
+
sort_menu_open = false
270
270
+
}}
271
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
272
+
option.key
273
273
+
? 'font-semibold'
274
274
+
: ''}"
275
275
+
>
276
276
+
<span>{option.label}</span>
277
277
+
<span class="text-neutral-400 dark:text-neutral-600">
278
278
+
{sort_indicator(option.key)}
279
279
+
</span>
280
280
+
</button>
281
281
+
{/each}
282
282
+
</div>
283
283
+
</details>
284
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
367
-
<div class="no-scrollbar flex items-center gap-1.5 overflow-x-auto px-4 py-2">
368
368
-
<div class="relative shrink-0">
414
414
+
<div class="no-scrollbar flex w-full items-center gap-1.5 overflow-x-auto px-4 py-2">
415
415
+
<div class="relative flex grow">
369
416
<span
370
370
-
class="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-xs text-neutral-500 select-none"
417
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
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
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
380
-
<div class="mx-0.5 h-4 w-px shrink-0 bg-neutral-100 dark:bg-neutral-800"></div>
381
381
-
<button
382
382
-
type="button"
383
383
-
onclick={() => toggle_sort('name')}
384
384
-
class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key ===
385
385
-
'name'
386
386
-
? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100'
387
387
-
: 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}"
388
388
-
>
389
389
-
Name{sort_indicator('name')}
390
390
-
</button>
391
391
-
<button
392
392
-
type="button"
393
393
-
onclick={() => toggle_sort('artist')}
394
394
-
class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key ===
395
395
-
'artist'
396
396
-
? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100'
397
397
-
: 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}"
398
398
-
>
399
399
-
Artist{sort_indicator('artist')}
400
400
-
</button>
401
401
-
<button
402
402
-
type="button"
403
403
-
onclick={() => toggle_sort('dateAdded', true)}
404
404
-
class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key ===
405
405
-
'dateAdded'
406
406
-
? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100'
407
407
-
: 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}"
408
408
-
>
409
409
-
Date Added{sort_indicator('dateAdded')}
410
410
-
</button>
411
411
-
<button
412
412
-
type="button"
413
413
-
onclick={() => toggle_sort('playCount', true)}
414
414
-
class="shrink-0 rounded border px-2.5 py-1 text-xs transition-colors {tracks_page_options.sort_key ===
415
415
-
'playCount'
416
416
-
? 'border-neutral-400 bg-neutral-200 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100'
417
417
-
: 'border-neutral-300 text-neutral-500 hover:text-neutral-600 dark:border-neutral-700 dark:hover:text-neutral-300'}"
418
418
-
>
419
419
-
Plays{sort_indicator('playCount')}
420
420
-
</button>
421
421
-
<span
422
422
-
class="ml-auto shrink-0 pl-1 text-xs text-neutral-400 tabular-nums dark:text-neutral-600"
423
423
-
>
427
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
428
-
<div class="no-scrollbar flex items-center gap-1.5 overflow-x-auto px-4 pb-2">
429
429
-
<!-- <button
432
432
+
<!-- <div class="no-scrollbar flex items-center gap-1.5 overflow-x-auto px-4 pb-2">
433
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
439
-
<!-- {#each genres as genre}
443
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
450
-
{/each} -->
451
451
-
</div>
454
454
+
{/each}
455
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
536
+
}
537
537
+
summary::-webkit-details-marker {
538
538
+
display: none;
532
539
}
533
540
</style>