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

Add light mode

Kasper (Mar 5, 2026, 1:38 PM +0100) 4cabf15b b54fcc35

+44 -46
-1
mobile/src/lib/commands.ts
··· 1 - // utils.ts 2 1 import { commands } from '../../bindings' 3 2 4 3 export default new Proxy({} as typeof commands, {
+4 -4
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 scheme-dark"> 40 + <div class="flex flex-col h-screen bg-white dark:bg-neutral-950 text-neutral-800 dark:text-neutral-200 text-sm overflow-hidden scheme-light-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} ··· 47 47 type="button" 48 48 onclick={open_library} 49 49 disabled={loading} 50 - class="shrink-0 px-3 py-1.5 bg-neutral-100 text-neutral-900 rounded-lg font-semibold text-xs hover:bg-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors" 50 + class="shrink-0 px-3 py-1.5 bg-neutral-900 text-neutral-100 dark:bg-neutral-100 dark:text-neutral-900 rounded-lg font-semibold text-xs hover:bg-neutral-700 dark:hover:bg-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors" 51 51 > 52 52 {loading ? 'Loading…' : 'Open'} 53 53 </button> ··· 57 57 {#if library} 58 58 <Library {library} {open_button} /> 59 59 {:else} 60 - <div class="flex flex-col items-center justify-center flex-1 gap-4 text-neutral-700 px-8 text-center"> 60 + <div class="flex flex-col items-center justify-center flex-1 gap-4 text-neutral-400 dark:text-neutral-700 px-8 text-center"> 61 61 <span class="text-5xl">♪</span> 62 62 <div> 63 - <p class="font-medium text-neutral-500">No library loaded</p> 63 + <p class="font-medium text-neutral-500 dark:text-neutral-500">No library loaded</p> 64 64 <div class="mt-4"> 65 65 {@render open_button()} 66 66 </div>
+32 -32
mobile/src/routes/Library.svelte
··· 163 163 } 164 164 </script> 165 165 166 - <div class="flex flex-col h-screen bg-neutral-950 text-neutral-200 text-sm overflow-hidden"> 166 + <div class="flex flex-col h-screen bg-white dark:bg-neutral-950 text-neutral-800 dark:text-neutral-200 text-sm overflow-hidden"> 167 167 168 168 <!-- Header --> 169 - <header class="flex items-center gap-2 px-4 py-3 border-b border-neutral-800 shrink-0"> 169 + <header class="flex items-center gap-2 px-4 py-3 border-b border-neutral-200 dark:border-neutral-800 shrink-0"> 170 170 {#if page.url.searchParams.has('id')} 171 171 <button 172 172 type="button" 173 173 onclick={() => history.back()} 174 - class="shrink-0 flex items-center justify-center w-8 h-8 text-xl rounded-lg text-neutral-400 hover:bg-neutral-800 hover:text-neutral-100 transition-colors" 174 + class="shrink-0 flex items-center justify-center w-8 h-8 text-xl rounded-lg text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors" 175 175 aria-label="Back" 176 176 >‹</button> 177 177 {/if} 178 178 179 179 <div class="flex-1 min-w-0"> 180 180 {#if view.kind === 'browser'} 181 - <p class="font-semibold text-neutral-100 truncate leading-tight">{node_name(view.folder_id)}</p> 181 + <p class="font-semibold text-neutral-900 dark:text-neutral-100 truncate leading-tight">{node_name(view.folder_id)}</p> 182 182 {:else if view.kind === 'tracks'} 183 - <p class="font-semibold text-neutral-100 truncate leading-tight">{node_name(view.playlist_id)}</p> 183 + <p class="font-semibold text-neutral-900 dark:text-neutral-100 truncate leading-tight">{node_name(view.playlist_id)}</p> 184 184 {/if} 185 185 186 186 </div> ··· 189 189 </header> 190 190 191 191 {#if error} 192 - <div class="px-4 py-2.5 bg-red-950 border-b border-red-900 text-red-400 text-xs shrink-0">⚠ {error}</div> 192 + <div class="px-4 py-2.5 bg-red-50 dark:bg-red-950 border-b border-red-200 dark:border-red-900 text-red-600 dark:text-red-400 text-xs shrink-0">⚠ {error}</div> 193 193 {/if} 194 194 195 195 <!-- ── Browser view ───────────────────────────────────────────────────── --> 196 196 {#if view.kind === 'browser'} 197 197 <div class="flex-1 overflow-y-auto"> 198 198 {#if current_children.length > 0} 199 - <ul class="divide-y divide-neutral-900"> 199 + <ul class="divide-y divide-neutral-200 dark:divide-neutral-900"> 200 200 {#each current_children as child_id} 201 201 {@const tl = get_tracklist(child_id)} 202 202 {#if tl?.type === 'folder'} ··· 204 204 <button 205 205 type="button" 206 206 onclick={() => open_folder(child_id)} 207 - class="w-full flex items-center gap-3 px-4 py-3.5 hover:bg-neutral-800/50 active:bg-neutral-800 transition-colors text-left" 207 + class="w-full flex items-center gap-3 px-4 py-3.5 hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 active:bg-neutral-100 dark:active:bg-neutral-800 transition-colors text-left" 208 208 > 209 209 <span class="shrink-0 w-7 text-center text-neutral-500 select-none"> 210 210 <svg ··· 220 220 > 221 221 </span> 222 222 <div class="flex-1 min-w-0"> 223 - <p class="font-medium text-neutral-100 truncate">{tl.name}</p> 223 + <p class="font-medium text-neutral-900 dark:text-neutral-100 truncate">{tl.name}</p> 224 224 <p class="text-xs text-neutral-500 mt-0.5"> 225 225 {tl.children.length} {tl.children.length === 1 ? 'item' : 'items'} · {count_tracks_in(child_id)} tracks 226 226 </p> 227 227 </div> 228 - <span class="text-neutral-700 text-lg select-none">›</span> 228 + <span class="text-neutral-400 dark:text-neutral-700 text-lg select-none">›</span> 229 229 </button> 230 230 </li> 231 231 {:else if tl?.type === 'playlist'} ··· 233 233 <button 234 234 type="button" 235 235 onclick={() => open_playlist(child_id)} 236 - class="w-full flex items-center gap-3 px-4 py-3.5 hover:bg-neutral-800/50 active:bg-neutral-800 transition-colors text-left" 236 + class="w-full flex items-center gap-3 px-4 py-3.5 hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 active:bg-neutral-100 dark:active:bg-neutral-800 transition-colors text-left" 237 237 > 238 238 <span class="shrink-0 w-7 text-center text-neutral-500 select-none"> 239 239 <svg ··· 249 249 > 250 250 </span> 251 251 <div class="flex-1 min-w-0"> 252 - <p class="font-medium text-neutral-100 truncate">{tl.name}</p> 252 + <p class="font-medium text-neutral-900 dark:text-neutral-100 truncate">{tl.name}</p> 253 253 <p class="text-xs text-neutral-500 mt-0.5"> 254 254 {tl.tracks.length} {tl.tracks.length === 1 ? 'track' : 'tracks'}{tl.liked ? ' · ♥' : ''} 255 255 </p> 256 256 </div> 257 - <span class="text-neutral-700 text-lg select-none">›</span> 257 + <span class="text-neutral-400 dark:text-neutral-700 text-lg select-none">›</span> 258 258 </button> 259 259 </li> 260 260 {/if} 261 261 {/each} 262 262 </ul> 263 263 {:else} 264 - <div class="flex items-center justify-center h-32 text-neutral-700 text-xs">This folder is empty</div> 264 + <div class="flex items-center justify-center h-32 text-neutral-400 dark:text-neutral-700 text-xs">This folder is empty</div> 265 265 {/if} 266 266 </div> 267 267 268 268 <!-- ── Tracks view ────────────────────────────────────────────────────── --> 269 269 {:else if view.kind === 'tracks'} 270 - <div class="shrink-0 border-b border-neutral-800"> 270 + <div class="shrink-0 border-b border-neutral-200 dark:border-neutral-800"> 271 271 <div class="flex items-center gap-1.5 px-4 py-2 overflow-x-auto no-scrollbar"> 272 272 <div class="relative shrink-0"> 273 273 <span class="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none text-xs select-none">⌕</span> ··· 275 275 type="search" 276 276 placeholder="Search…" 277 277 bind:value={search_query} 278 - class="pl-7 pr-3 py-1.5 w-32 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 text-xs placeholder-neutral-600 outline-none focus:border-neutral-500 transition-colors" 278 + class="pl-7 pr-3 py-1.5 w-32 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-lg text-neutral-800 dark:text-neutral-200 text-xs placeholder-neutral-400 dark:placeholder-neutral-600 outline-none focus:border-neutral-400 dark:focus:border-neutral-500 transition-colors" 279 279 /> 280 280 </div> 281 - <div class="w-px h-4 bg-neutral-800 shrink-0 mx-0.5"></div> 281 + <div class="w-px h-4 bg-neutral-100 dark:bg-neutral-800 shrink-0 mx-0.5"></div> 282 282 <button type="button" onclick={() => toggle_sort('name')} 283 - class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'name' ? 'bg-neutral-700 border-neutral-600 text-neutral-100' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 283 + class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'name' ? 'bg-neutral-200 dark:bg-neutral-700 border-neutral-400 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 284 284 Name{sort_indicator('name')} 285 285 </button> 286 286 <button type="button" onclick={() => toggle_sort('artist')} 287 - class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'artist' ? 'bg-neutral-700 border-neutral-600 text-neutral-100' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 287 + class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'artist' ? 'bg-neutral-200 dark:bg-neutral-700 border-neutral-400 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 288 288 Artist{sort_indicator('artist')} 289 289 </button> 290 290 <button type="button" onclick={() => toggle_sort('dateAdded')} 291 - class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'dateAdded' ? 'bg-neutral-700 border-neutral-600 text-neutral-100' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 291 + class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'dateAdded' ? 'bg-neutral-200 dark:bg-neutral-700 border-neutral-400 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 292 292 Date{sort_indicator('dateAdded')} 293 293 </button> 294 294 <button type="button" onclick={() => toggle_sort('playCount')} 295 - class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'playCount' ? 'bg-neutral-700 border-neutral-600 text-neutral-100' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 295 + class="shrink-0 px-2.5 py-1 rounded text-xs border transition-colors {sort_key === 'playCount' ? 'bg-neutral-200 dark:bg-neutral-700 border-neutral-400 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 296 296 Plays{sort_indicator('playCount')} 297 297 </button> 298 - <span class="ml-auto shrink-0 text-xs text-neutral-600 tabular-nums pl-1"> 298 + <span class="ml-auto shrink-0 text-xs text-neutral-400 dark:text-neutral-600 tabular-nums pl-1"> 299 299 {filtered_tracks.length}/{playlist_tracks.length} 300 300 </span> 301 301 </div> 302 302 303 303 <div class="flex items-center gap-1.5 px-4 pb-2 overflow-x-auto no-scrollbar"> 304 304 <button type="button" onclick={() => active_filter = { kind: 'all' }} 305 - class="shrink-0 px-2.5 py-1 rounded-full text-xs border transition-colors {active_filter.kind === 'all' ? 'bg-neutral-200 border-neutral-200 text-neutral-900 font-semibold' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 305 + class="shrink-0 px-2.5 py-1 rounded-full text-xs border transition-colors {active_filter.kind === 'all' ? 'bg-neutral-200 border-neutral-200 text-neutral-900 font-semibold' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 306 306 All 307 307 </button> 308 308 <button type="button" onclick={() => active_filter = { kind: 'liked' }} 309 - class="shrink-0 px-2.5 py-1 rounded-full text-xs border transition-colors {active_filter.kind === 'liked' ? 'bg-rose-900 border-rose-700 text-rose-200 font-semibold' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 309 + class="shrink-0 px-2.5 py-1 rounded-full text-xs border transition-colors {active_filter.kind === 'liked' ? 'bg-rose-100 dark:bg-rose-900 border-rose-300 dark:border-rose-700 text-rose-700 dark:text-rose-200 font-semibold' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 310 310 ♥ Liked 311 311 </button> 312 312 {#each genres as genre} 313 313 <button type="button" onclick={() => active_filter = { kind: 'genre', value: genre }} 314 - class="shrink-0 px-2.5 py-1 rounded-full text-xs border transition-colors {active_filter.kind === 'genre' && active_filter.value === genre ? 'bg-neutral-700 border-neutral-600 text-neutral-100 font-semibold' : 'border-neutral-700 text-neutral-500 hover:text-neutral-300'}"> 314 + class="shrink-0 px-2.5 py-1 rounded-full text-xs border transition-colors {active_filter.kind === 'genre' && active_filter.value === genre ? 'bg-neutral-200 dark:bg-neutral-700 border-neutral-400 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100 font-semibold' : 'border-neutral-300 dark:border-neutral-700 text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300'}"> 315 315 {genre} 316 316 </button> 317 317 {/each} ··· 320 320 321 321 <div class="flex-1 overflow-y-auto"> 322 322 {#if filtered_tracks.length > 0} 323 - <ul class="divide-y divide-neutral-900"> 323 + <ul class="divide-y divide-neutral-200 dark:divide-neutral-900"> 324 324 {#each filtered_tracks as track} 325 325 <li class="flex items-center gap-3 px-4 py-3 transition-colors"> 326 326 <div class="flex-1 min-w-0"> 327 - <p class="font-medium text-neutral-100 truncate">{track.name}</p> 327 + <p class="font-medium text-neutral-900 dark:text-neutral-100 truncate">{track.name}</p> 328 328 <p class="text-xs text-neutral-500 truncate mt-0.5"> 329 329 {track.artist ?? 'Unknown Artist'} 330 330 {#if track.albumName} 331 - <span class="text-neutral-700"> · </span>{track.albumName} 331 + <span class="text-neutral-400 dark:text-neutral-700"> · </span>{track.albumName} 332 332 {/if} 333 333 </p> 334 334 <div class="flex items-center gap-2 mt-1"> 335 335 {#if track.genre} 336 - <span class="px-1.5 py-px text-xs rounded bg-neutral-800 text-neutral-500">{track.genre}</span> 336 + <span class="px-1.5 py-px text-xs rounded bg-neutral-100 dark:bg-neutral-800 text-neutral-500">{track.genre}</span> 337 337 {/if} 338 338 {#if track.year} 339 - <span class="text-xs text-neutral-700">{track.year}</span> 339 + <span class="text-xs text-neutral-400 dark:text-neutral-700">{track.year}</span> 340 340 {/if} 341 341 </div> 342 342 </div> 343 - <div class="shrink-0 flex flex-col items-end gap-1 text-xs text-neutral-600 tabular-nums"> 343 + <div class="shrink-0 flex flex-col items-end gap-1 text-xs text-neutral-400 dark:text-neutral-600 tabular-nums"> 344 344 <span>{format_duration(track.duration)}</span> 345 345 {#if track.playCount} 346 346 <span>{track.playCount} plays</span> ··· 353 353 {/each} 354 354 </ul> 355 355 {:else} 356 - <div class="flex flex-col items-center justify-center h-full gap-3 text-neutral-700 px-8 text-center"> 356 + <div class="flex flex-col items-center justify-center h-full gap-3 text-neutral-400 dark:text-neutral-700 px-8 text-center"> 357 357 <span class="text-4xl">⌕</span> 358 358 <p class="text-xs">No tracks match your search or filter</p> 359 359 </div>
+8 -9
mobile/vite.config.ts
··· 1 - import tailwindcss from '@tailwindcss/vite'; 2 - import { defineConfig } from 'vite'; 3 - import { sveltekit } from '@sveltejs/kit/vite'; 1 + import tailwindcss from '@tailwindcss/vite' 2 + import { defineConfig } from 'vite' 3 + import { sveltekit } from '@sveltejs/kit/vite' 4 4 5 - // @ts-expect-error process is a nodejs global 6 - const host = process.env.TAURI_DEV_HOST; 5 + const host = process.env.TAURI_DEV_HOST 7 6 8 7 // https://vite.dev/config/ 9 8 export default defineConfig(async () => ({ ··· 22 21 fs: { allow: ['./bindings.ts'] }, 23 22 watch: { 24 23 // 3. tell Vite to ignore watching `src-tauri` 25 - ignored: ['**/src-tauri/**'] 26 - } 27 - } 28 - })); 24 + ignored: ['**/src-tauri/**'], 25 + }, 26 + }, 27 + }))