[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.

Functioning library browser

Kasper (Mar 5, 2026, 1:28 PM +0100) e99dd254 99f3f801

+29 -17
+1 -1
mobile/bindings.ts
··· 38 38 * For example iTunes Persistent ID 39 39 */ 40 40 originalId?: string | null; dateImported?: string | null; dateCreated?: string | null; children: string[] } 41 - export type LibraryTauri = { tracks: Partial<{ [key in string]: Track }>; track_lists: Partial<{ [key in string]: TrackList }> } 41 + export type LibraryTauri = { tracks: Partial<{ [key in string]: Track }>; track_item_ids: Partial<{ [key in string]: number }>; track_lists: Partial<{ [key in string]: TrackList }> } 42 42 export type Playlist = { id: string; name: string; description?: string | null; liked: boolean; disliked: boolean; importedFrom?: string | null; originalId?: string | null; dateImported?: string | null; dateCreated?: string | null; tracks: number[] } 43 43 export type Special = { id: string; name: SpecialTrackListName; dateCreated: string; children: string[] } 44 44 export type SpecialTrackListName = "Root"
+3 -1
mobile/src-tauri/src/lib.rs
··· 1 1 use anyhow::bail; 2 + use ferrum::library_types::{ItemId, TrackID, VersionedLibrary}; 2 3 use ferrum::library_types::{Library, Track, TrackList, TrackListID}; 3 - use ferrum::library_types::{TrackID, VersionedLibrary}; 4 4 use serde::Serialize; 5 5 use specta::Type; 6 6 use std::collections::HashMap; ··· 45 45 #[derive(Serialize, Type)] 46 46 pub struct LibraryTauri { 47 47 tracks: HashMap<TrackID, Track>, 48 + track_item_ids: HashMap<TrackID, ItemId>, 48 49 track_lists: HashMap<TrackListID, TrackList>, 49 50 } 50 51 ··· 58 59 59 60 let library_tauri = LibraryTauri { 60 61 tracks: library.get_tracks().clone().into_iter().collect(), 62 + track_item_ids: library.get_track_item_ids().clone().into_iter().collect(), 61 63 track_lists: library.trackLists.into_iter().collect(), 62 64 }; 63 65
+1 -1
mobile/src/routes/+page.svelte
··· 37 37 } 38 38 </script> 39 39 40 - <div class="flex flex-col h-screen bg-neutral-950 text-neutral-200 text-sm overflow-hidden"> 40 + <div class="flex flex-col h-screen bg-neutral-950 text-neutral-200 text-sm overflow-hidden scheme-dark"> 41 41 {#if error} 42 42 <div class="px-4 py-2.5 bg-red-950 border-b border-red-900 text-red-400 text-xs shrink-0">⚠ {error}</div> 43 43 {/if}
+20 -14
mobile/src/routes/Library.svelte
··· 105 105 } 106 106 } 107 107 108 - // ── Derived data ─────────────────────────────────────────────────────────── 109 - 110 - const all_tracks = $derived( 111 - Object.values(library?.tracks ?? {}).filter((t): t is Track => t !== null) 112 - ); 108 + // ── Derived / async data ────────────────────────────────────────────────── 113 109 114 110 const current_children = $derived( 115 111 view.kind === 'browser' ? get_children(view.folder_id) : [] 116 112 ); 117 113 118 - const playlist_tracks = $derived( 119 - view.kind !== 'tracks' 120 - ? [] 121 - : (get_playlist(view.playlist_id)?.tracks ?? []) 122 - .map(item_id => all_tracks[item_id]) 123 - .filter((t): t is Track => t !== null) 124 - ); 114 + // Playlist.tracks is typed as number[] in the bindings, but serialize_playlist_ids 115 + // in Rust serializes them back to track ID strings over the wire. 116 + const playlist_tracks = $derived.by(() => { 117 + if (view.kind !== 'tracks') return []; 118 + const playlist = get_playlist(view.playlist_id); 119 + if (!playlist) { 120 + console.error('[Library] no playlist found for id', view.playlist_id); 121 + return []; 122 + } 123 + const track_ids = playlist.tracks as unknown as string[]; 124 + const resolved = track_ids.map(track_id => { 125 + const track = library?.tracks?.[track_id]; 126 + if (track === undefined) console.error('[Library] no track for track_id', track_id); 127 + return track; 128 + }).filter((t): t is Track => t !== undefined); 129 + return resolved; 130 + }); 125 131 126 132 const genres = $derived( 127 - [...new Set(playlist_tracks.map(t => t.genre).filter((g): g is string => g !== null))].sort() 133 + [...new Set(playlist_tracks.map(t => t.genre).filter((g): g is string => g !== null && g !== undefined))].sort() 128 134 ); 129 135 130 136 const filtered_tracks = $derived( ··· 335 341 {#if filtered_tracks.length > 0} 336 342 <ul class="divide-y divide-neutral-900"> 337 343 {#each filtered_tracks as track} 338 - <li class="flex items-center gap-3 px-4 py-3 hover:bg-neutral-800/50 active:bg-neutral-800 transition-colors"> 344 + <li class="flex items-center gap-3 px-4 py-3 transition-colors"> 339 345 <div class="flex-1 min-w-0"> 340 346 <p class="font-medium text-neutral-100 truncate">{track.name}</p> 341 347 <p class="text-xs text-neutral-500 truncate mt-0.5">
+4
mobile/src/routes/layout.css
··· 1 1 @import 'tailwindcss'; 2 + 3 + @theme { 4 + --default-transition-timing-function: cubic-bezier(0.33, 1, 0.68, 1); /* cubic-out */ 5 + }