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

Show library tracks in app

Kasper (Mar 5, 2026, 1:28 PM +0100) 71af67b2 5dd36bf9

+96 -149
+2
Cargo.lock
··· 1144 1144 "log", 1145 1145 "serde", 1146 1146 "serde_json", 1147 + "simd-json", 1147 1148 "specta", 1148 1149 "specta-typescript", 1149 1150 "tauri", 1150 1151 "tauri-build", 1151 1152 "tauri-plugin-dialog", 1153 + "tauri-plugin-fs", 1152 1154 "tauri-plugin-opener", 1153 1155 "tauri-specta", 1154 1156 ]
+10
mobile/package-lock.json
··· 11 11 "dependencies": { 12 12 "@tauri-apps/api": "^2", 13 13 "@tauri-apps/plugin-dialog": "^2.6.0", 14 + "@tauri-apps/plugin-fs": "^2.4.5", 14 15 "@tauri-apps/plugin-opener": "^2" 15 16 }, 16 17 "devDependencies": { ··· 1489 1490 "version": "2.6.0", 1490 1491 "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", 1491 1492 "integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==", 1493 + "license": "MIT OR Apache-2.0", 1494 + "dependencies": { 1495 + "@tauri-apps/api": "^2.8.0" 1496 + } 1497 + }, 1498 + "node_modules/@tauri-apps/plugin-fs": { 1499 + "version": "2.4.5", 1500 + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz", 1501 + "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==", 1492 1502 "license": "MIT OR Apache-2.0", 1493 1503 "dependencies": { 1494 1504 "@tauri-apps/api": "^2.8.0"
+1
mobile/package.json
··· 17 17 "dependencies": { 18 18 "@tauri-apps/api": "^2", 19 19 "@tauri-apps/plugin-dialog": "^2.6.0", 20 + "@tauri-apps/plugin-fs": "^2.4.5", 20 21 "@tauri-apps/plugin-opener": "^2" 21 22 }, 22 23 "devDependencies": {
+2
mobile/src-tauri/Cargo.toml
··· 25 25 specta-typescript = "0.0.9" 26 26 specta = "=2.0.0-rc.22" 27 27 tauri-plugin-dialog = "2" 28 + simd-json = "0.16.0" 29 + tauri-plugin-fs = "2" 28 30 29 31 [target.'cfg(target_os = "android")'.dependencies] 30 32 android_logger = "0.15.1"
+2 -1
mobile/src-tauri/capabilities/default.json
··· 8 8 "permissions": [ 9 9 "core:default", 10 10 "opener:default", 11 - "dialog:default" 11 + "dialog:default", 12 + "fs:default" 12 13 ] 13 14 }
+29 -5
mobile/src-tauri/src/lib.rs
··· 1 - use ferrum::library::load_library_from_file; 2 - use ferrum::library_types::Track; 1 + use anyhow::bail; 2 + use ferrum::library_types::VersionedLibrary; 3 + use ferrum::library_types::{Library, Track}; 4 + use std::time::Instant; 3 5 use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; 4 6 5 7 #[tauri::command] ··· 15 17 .show(|_| {}); 16 18 } 17 19 20 + fn load_library_from_file(library_json: &str) -> anyhow::Result<Library> { 21 + let now = Instant::now(); 22 + 23 + let mut json_bytes = library_json.as_bytes().to_vec(); 24 + 25 + let versioned_library: VersionedLibrary = match simd_json::from_slice(&mut json_bytes) { 26 + Ok(lib) => { 27 + println!("Parsed library: {}ms", now.elapsed().as_millis()); 28 + lib 29 + } 30 + Err(err) => { 31 + bail!("Error parsing library: {}", err); 32 + } 33 + }; 34 + let now = Instant::now(); 35 + 36 + let library = versioned_library.upgrade().init_libary(); 37 + println!("Initialized library: {}ms", now.elapsed().as_millis()); 38 + Ok(library) 39 + } 40 + 18 41 #[tauri::command] 19 42 #[specta::specta] 20 43 fn load_library(library_json: String) -> Result<Vec<Track>, String> { 44 + println!("Loadeding... -------------- {library_json}"); 21 45 let library = match load_library_from_file(&library_json) { 22 46 Ok(library) => library, 23 47 Err(err) => return Err(err.to_string()), 24 48 }; 25 49 26 - let tracks = library.get_tracks().values().cloned().collect(); 50 + let tracks: Vec<_> = library.get_tracks().values().cloned().collect(); 51 + println!("Loaded {} tracks", tracks.len()); 27 52 Ok(tracks) 28 53 } 29 54 ··· 36 61 .with_tag("{{app.name}}"), 37 62 ); 38 63 39 - println!("-------------------- RUN"); 40 64 let specta_builder = tauri_specta::Builder::<tauri::Wry>::new() 41 65 .commands(tauri_specta::collect_commands![error_popup, load_library]); 42 - println!("-------------------- run"); 43 66 44 67 #[cfg(all(debug_assertions, not(target_os = "android")))] 45 68 #[cfg(debug_assertions)] ··· 52 75 .expect("Failed to export typescript bindings"); 53 76 54 77 tauri::Builder::default() 78 + .plugin(tauri_plugin_fs::init()) 55 79 .plugin(tauri_plugin_dialog::init()) 56 80 .plugin(tauri_plugin_opener::init()) 57 81 .invoke_handler(specta_builder.invoke_handler())
+50 -143
mobile/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { commands } from '../../bindings'; 2 + import commands from '$lib/commands' 3 + import { open } from '@tauri-apps/plugin-dialog'; 4 + import type { Track } from '../../bindings' 5 + import { readTextFile } from '@tauri-apps/plugin-fs' 3 6 7 + let tracks: Track[] = []; 8 + let loading = false; 9 + let error = ''; 10 + let msg = '' 4 11 5 - async function greet(event: Event) { 6 - event.preventDefault(); 7 - const result = await commands.loadLibrary(library_json_path); 12 + async function open_library() { 13 + msg = 'Loading...' 14 + const path = await open({ 15 + filters: [{ name: 'JSON', extensions: ['json'] }], 16 + }); 17 + if (!path) return; 18 + 19 + loading = true; 20 + error = ''; 21 + const contents = await readTextFile(path); 22 + const result = await commands.loadLibrary(contents); 23 + msg = 'Loaded' + JSON.stringify(result) 24 + if (result.status === 'ok') { 25 + tracks = result.data; 26 + } else { 27 + error = result.error; 28 + } 8 29 } 9 30 </script> 10 31 11 - <main class="container"> 12 - <h1>Welcome to Tauri + Svelte</h1> 32 + <main class="p-6"> 33 + <button 34 + type='button' 35 + onclick={open_library} 36 + disabled={loading} 37 + class="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50" 38 + > 39 + {loading ? 'Loading...' : 'Open Library'} 40 + </button> 13 41 14 - <div class="row"> 15 - <a href="https://vite.dev" target="_blank"> 16 - <img src="/vite.svg" class="logo vite" alt="Vite Logo" /> 17 - </a> 18 - <a href="https://tauri.app" target="_blank"> 19 - <img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" /> 20 - </a> 21 - <a href="https://svelte.dev" target="_blank"> 22 - <img src="/svelte.svg" class="logo svelte-kit" alt="SvelteKit Logo" /> 23 - </a> 24 - </div> 25 - <p>Click on the Tauri, Vite, and SvelteKit logos to learn more.</p> 42 + {#if error} 43 + <p class="mt-4 text-red-500">{error}</p> 44 + {/if} 26 45 27 - <form class="row" onsubmit={greet}> 28 - <input id="greet-input" placeholder="Enter a name..." /> 29 - <button type="submit">Greet</button> 30 - </form> 46 + {#if tracks.length > 0} 47 + <p class="mt-4 text-sm text-gray-500">{tracks.length} tracks</p> 48 + <ul class="mt-2 divide-y divide-gray-200"> 49 + {#each tracks as track} 50 + <li class="py-3"> 51 + <p class="font-medium">{track.name}</p> 52 + {#if track.artist} 53 + <p class="text-sm text-gray-500">{track.artist}</p> 54 + {/if} 55 + </li> 56 + {/each} 57 + </ul> 58 + {/if} 31 59 </main> 32 - 33 - <style> 34 - .logo.vite:hover { 35 - filter: drop-shadow(0 0 2em #747bff); 36 - } 37 - 38 - .logo.svelte-kit:hover { 39 - filter: drop-shadow(0 0 2em #ff3e00); 40 - } 41 - 42 - :root { 43 - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 44 - font-size: 16px; 45 - line-height: 24px; 46 - font-weight: 400; 47 - 48 - color: #0f0f0f; 49 - background-color: #f6f6f6; 50 - 51 - font-synthesis: none; 52 - text-rendering: optimizeLegibility; 53 - -webkit-font-smoothing: antialiased; 54 - -moz-osx-font-smoothing: grayscale; 55 - -webkit-text-size-adjust: 100%; 56 - } 57 - 58 - .container { 59 - margin: 0; 60 - padding-top: 10vh; 61 - display: flex; 62 - flex-direction: column; 63 - justify-content: center; 64 - text-align: center; 65 - } 66 - 67 - .logo { 68 - height: 6em; 69 - padding: 1.5em; 70 - will-change: filter; 71 - transition: 0.75s; 72 - } 73 - 74 - .logo.tauri:hover { 75 - filter: drop-shadow(0 0 2em #24c8db); 76 - } 77 - 78 - .row { 79 - display: flex; 80 - justify-content: center; 81 - } 82 - 83 - a { 84 - font-weight: 500; 85 - color: #646cff; 86 - text-decoration: inherit; 87 - } 88 - 89 - a:hover { 90 - color: #535bf2; 91 - } 92 - 93 - h1 { 94 - text-align: center; 95 - } 96 - 97 - input, 98 - button { 99 - border-radius: 8px; 100 - border: 1px solid transparent; 101 - padding: 0.6em 1.2em; 102 - font-size: 1em; 103 - font-weight: 500; 104 - font-family: inherit; 105 - color: #0f0f0f; 106 - background-color: #ffffff; 107 - transition: border-color 0.25s; 108 - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 109 - } 110 - 111 - button { 112 - cursor: pointer; 113 - } 114 - 115 - button:hover { 116 - border-color: #396cd8; 117 - } 118 - button:active { 119 - border-color: #396cd8; 120 - background-color: #e8e8e8; 121 - } 122 - 123 - input, 124 - button { 125 - outline: none; 126 - } 127 - 128 - #greet-input { 129 - margin-right: 5px; 130 - } 131 - 132 - @media (prefers-color-scheme: dark) { 133 - :root { 134 - color: #f6f6f6; 135 - background-color: #2f2f2f; 136 - } 137 - 138 - a:hover { 139 - color: #24c8db; 140 - } 141 - 142 - input, 143 - button { 144 - color: #ffffff; 145 - background-color: #0f0f0f98; 146 - } 147 - button:active { 148 - background-color: #0f0f0f69; 149 - } 150 - } 151 - 152 - </style>