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

Fix error loading library after restart

Kasper (Mar 5, 2026, 5:06 PM +0100) d509d68b bd2ff2fc

+243 -12
+1 -1
.github/workflows/ci.yml
··· 98 98 projectPath: mobile 99 99 uploadWorkflowArtifacts: true 100 100 mobile: android 101 - args: --verbose --apk --split-per-abi --target ${{ matrix.tauri_target }} 101 + args: --apk --split-per-abi --target ${{ matrix.tauri_target }}
+1 -1
.github/workflows/release.yml
··· 112 112 mobile: android 113 113 releaseName: ${{ github.ref_name }} 114 114 releaseDraft: true 115 - args: --verbose --apk --split-per-abi --target ${{ matrix.tauri_target }} 115 + args: --apk --split-per-abi --target ${{ matrix.tauri_target }}
+126
Cargo.lock
··· 1149 1149 "specta-typescript", 1150 1150 "tauri", 1151 1151 "tauri-build", 1152 + "tauri-plugin-android-fs", 1152 1153 "tauri-plugin-dialog", 1153 1154 "tauri-plugin-fs", 1154 1155 "tauri-plugin-opener", 1156 + "tauri-plugin-os", 1155 1157 "tauri-plugin-safe-area-insets-css", 1156 1158 "tauri-plugin-store", 1157 1159 "tauri-specta", ··· 1450 1452 dependencies = [ 1451 1453 "typenum", 1452 1454 "version_check", 1455 + ] 1456 + 1457 + [[package]] 1458 + name = "gethostname" 1459 + version = "1.0.2" 1460 + source = "registry+https://github.com/rust-lang/crates.io-index" 1461 + checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" 1462 + dependencies = [ 1463 + "rustix 1.1.2", 1464 + "windows-targets 0.52.6", 1453 1465 ] 1454 1466 1455 1467 [[package]] ··· 2538 2550 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2539 2551 2540 2552 [[package]] 2553 + name = "nix" 2554 + version = "0.30.1" 2555 + source = "registry+https://github.com/rust-lang/crates.io-index" 2556 + checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 2557 + dependencies = [ 2558 + "bitflags 2.9.4", 2559 + "cfg-if", 2560 + "cfg_aliases", 2561 + "libc", 2562 + ] 2563 + 2564 + [[package]] 2541 2565 name = "nodrop" 2542 2566 version = "0.1.14" 2543 2567 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2688 2712 ] 2689 2713 2690 2714 [[package]] 2715 + name = "objc2-core-location" 2716 + version = "0.3.1" 2717 + source = "registry+https://github.com/rust-lang/crates.io-index" 2718 + checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" 2719 + dependencies = [ 2720 + "objc2 0.6.2", 2721 + "objc2-foundation 0.3.1", 2722 + ] 2723 + 2724 + [[package]] 2691 2725 name = "objc2-encode" 2692 2726 version = "4.1.0" 2693 2727 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2781 2815 dependencies = [ 2782 2816 "bitflags 2.9.4", 2783 2817 "objc2 0.6.2", 2818 + "objc2-core-foundation", 2784 2819 "objc2-foundation 0.3.1", 2785 2820 ] 2786 2821 ··· 2802 2837 checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" 2803 2838 dependencies = [ 2804 2839 "bitflags 2.9.4", 2840 + "block2 0.6.2", 2805 2841 "objc2 0.6.2", 2842 + "objc2-cloud-kit", 2843 + "objc2-core-data", 2806 2844 "objc2-core-foundation", 2845 + "objc2-core-graphics", 2846 + "objc2-core-image", 2847 + "objc2-core-location", 2848 + "objc2-foundation 0.3.1", 2849 + "objc2-quartz-core 0.3.1", 2850 + "objc2-user-notifications", 2851 + ] 2852 + 2853 + [[package]] 2854 + name = "objc2-user-notifications" 2855 + version = "0.3.1" 2856 + source = "registry+https://github.com/rust-lang/crates.io-index" 2857 + checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" 2858 + dependencies = [ 2859 + "objc2 0.6.2", 2807 2860 "objc2-foundation 0.3.1", 2808 2861 ] 2809 2862 ··· 2876 2929 ] 2877 2930 2878 2931 [[package]] 2932 + name = "os_info" 2933 + version = "3.14.0" 2934 + source = "registry+https://github.com/rust-lang/crates.io-index" 2935 + checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" 2936 + dependencies = [ 2937 + "android_system_properties", 2938 + "log", 2939 + "nix", 2940 + "objc2 0.6.2", 2941 + "objc2-foundation 0.3.1", 2942 + "objc2-ui-kit", 2943 + "serde", 2944 + "windows-sys 0.61.0", 2945 + ] 2946 + 2947 + [[package]] 2879 2948 name = "pango" 2880 2949 version = "0.18.3" 2881 2950 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4124 4193 ] 4125 4194 4126 4195 [[package]] 4196 + name = "sync_async" 4197 + version = "0.1.0" 4198 + source = "registry+https://github.com/rust-lang/crates.io-index" 4199 + checksum = "b815ef8e053247ad785b136d233ee9c9872eeda5319f2719d57cac88270c3dd9" 4200 + dependencies = [ 4201 + "proc-macro2", 4202 + "quote", 4203 + "syn 2.0.106", 4204 + ] 4205 + 4206 + [[package]] 4127 4207 name = "sync_wrapper" 4128 4208 version = "1.0.2" 4129 4209 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4141 4221 "proc-macro2", 4142 4222 "quote", 4143 4223 "syn 2.0.106", 4224 + ] 4225 + 4226 + [[package]] 4227 + name = "sys-locale" 4228 + version = "0.3.2" 4229 + source = "registry+https://github.com/rust-lang/crates.io-index" 4230 + checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" 4231 + dependencies = [ 4232 + "libc", 4144 4233 ] 4145 4234 4146 4235 [[package]] ··· 4346 4435 ] 4347 4436 4348 4437 [[package]] 4438 + name = "tauri-plugin-android-fs" 4439 + version = "27.0.0" 4440 + source = "registry+https://github.com/rust-lang/crates.io-index" 4441 + checksum = "5897450c99d0ae467429522c6bfaa08fa991423a2b7eeca031b139fdfb6a0e50" 4442 + dependencies = [ 4443 + "base64 0.22.1", 4444 + "glob", 4445 + "percent-encoding", 4446 + "schemars 0.8.22", 4447 + "serde", 4448 + "serde_json", 4449 + "sync_async", 4450 + "tauri", 4451 + "tauri-plugin", 4452 + "tauri-plugin-fs", 4453 + "thiserror 2.0.16", 4454 + ] 4455 + 4456 + [[package]] 4349 4457 name = "tauri-plugin-dialog" 4350 4458 version = "2.6.0" 4351 4459 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4405 4513 "url", 4406 4514 "windows 0.61.3", 4407 4515 "zbus", 4516 + ] 4517 + 4518 + [[package]] 4519 + name = "tauri-plugin-os" 4520 + version = "2.3.2" 4521 + source = "registry+https://github.com/rust-lang/crates.io-index" 4522 + checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" 4523 + dependencies = [ 4524 + "gethostname", 4525 + "log", 4526 + "os_info", 4527 + "serde", 4528 + "serde_json", 4529 + "serialize-to-javascript", 4530 + "sys-locale", 4531 + "tauri", 4532 + "tauri-plugin", 4533 + "thiserror 2.0.16", 4408 4534 ] 4409 4535 4410 4536 [[package]]
+8
mobile/bindings.ts
··· 15 15 if(e instanceof Error) throw e; 16 16 else return { status: "error", error: e as any }; 17 17 } 18 + }, 19 + async openFilePersistentAndroid() : Promise<Result<string | null, string>> { 20 + try { 21 + return { status: "ok", data: await TAURI_INVOKE("open_file_persistent_android") }; 22 + } catch (e) { 23 + if(e instanceof Error) throw e; 24 + else return { status: "error", error: e as any }; 25 + } 18 26 } 19 27 } 20 28
+31 -1
mobile/package-lock.json
··· 13 13 "@tauri-apps/plugin-dialog": "^2.6.0", 14 14 "@tauri-apps/plugin-fs": "^2.4.5", 15 15 "@tauri-apps/plugin-opener": "^2", 16 - "@tauri-apps/plugin-store": "^2.4.2" 16 + "@tauri-apps/plugin-os": "^2.3.2", 17 + "@tauri-apps/plugin-store": "^2.4.2", 18 + "tauri-plugin-android-fs-api": "^27.0.0" 17 19 }, 18 20 "devDependencies": { 19 21 "@saurl/tauri-plugin-safe-area-insets-css-api": "^0.1.0", ··· 1524 1526 "@tauri-apps/api": "^2.8.0" 1525 1527 } 1526 1528 }, 1529 + "node_modules/@tauri-apps/plugin-os": { 1530 + "version": "2.3.2", 1531 + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz", 1532 + "integrity": "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==", 1533 + "license": "MIT OR Apache-2.0", 1534 + "dependencies": { 1535 + "@tauri-apps/api": "^2.8.0" 1536 + } 1537 + }, 1527 1538 "node_modules/@tauri-apps/plugin-store": { 1528 1539 "version": "2.4.2", 1529 1540 "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.2.tgz", ··· 1622 1633 "license": "MIT", 1623 1634 "engines": { 1624 1635 "node": ">= 0.6" 1636 + } 1637 + }, 1638 + "node_modules/create-web-stream": { 1639 + "version": "1.1.2", 1640 + "resolved": "https://registry.npmjs.org/create-web-stream/-/create-web-stream-1.1.2.tgz", 1641 + "integrity": "sha512-qtdh7eHXm5ityhyXhMGU1Jm7Wb49pLWo8OZsLGEm4Wf2sXADQXkouQxt6Pq4C8y3O0XyIVGf0Jun4ZbelxytoA==", 1642 + "license": "(MIT OR Apache-2.0)", 1643 + "engines": { 1644 + "node": ">=18.9.0" 1625 1645 } 1626 1646 }, 1627 1647 "node_modules/debug": { ··· 2362 2382 "funding": { 2363 2383 "type": "opencollective", 2364 2384 "url": "https://opencollective.com/webpack" 2385 + } 2386 + }, 2387 + "node_modules/tauri-plugin-android-fs-api": { 2388 + "version": "27.0.0", 2389 + "resolved": "https://registry.npmjs.org/tauri-plugin-android-fs-api/-/tauri-plugin-android-fs-api-27.0.0.tgz", 2390 + "integrity": "sha512-UW7/4lricWE3LUiw7QTfyY6LiGvtuP8vE8ai5FR5EkMztDwlpwEO9OT8BqSViWwwO5TU+E6BEt8PF+yEYsZtvg==", 2391 + "license": "(MIT OR Apache-2.0)", 2392 + "dependencies": { 2393 + "@tauri-apps/api": "^2.0.0", 2394 + "create-web-stream": "^1.1.1" 2365 2395 } 2366 2396 }, 2367 2397 "node_modules/tinyglobby": {
+3 -1
mobile/package.json
··· 21 21 "@tauri-apps/plugin-dialog": "^2.6.0", 22 22 "@tauri-apps/plugin-fs": "^2.4.5", 23 23 "@tauri-apps/plugin-opener": "^2", 24 - "@tauri-apps/plugin-store": "^2.4.2" 24 + "@tauri-apps/plugin-os": "^2.3.2", 25 + "@tauri-apps/plugin-store": "^2.4.2", 26 + "tauri-plugin-android-fs-api": "^27.0.0" 25 27 }, 26 28 "devDependencies": { 27 29 "@saurl/tauri-plugin-safe-area-insets-css-api": "^0.1.0",
+2
mobile/src-tauri/Cargo.toml
··· 30 30 tauri-plugin-fs = "2" 31 31 tauri-plugin-store = "2" 32 32 tauri-plugin-safe-area-insets-css = "0.2.0" 33 + tauri-plugin-android-fs = "27.0.0" 34 + tauri-plugin-os = "2" 33 35 34 36 [target.'cfg(target_os = "android")'.dependencies] 35 37 android_logger = "0.15.1"
+7 -1
mobile/src-tauri/capabilities/default.json
··· 8 8 "opener:default", 9 9 "dialog:default", 10 10 "fs:default", 11 + { 12 + "identifier": "fs:read-files", 13 + "allow": ["**"] 14 + }, 11 15 "store:default", 12 - "safe-area-insets-css:default" 16 + "safe-area-insets-css:default", 17 + "android-fs:default", 18 + "os:default" 13 19 ] 14 20 }
+3
mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml
··· 34 34 android:resource="@xml/file_paths" /> 35 35 </provider> 36 36 </application> 37 + <!-- ANDROID FS PLUGIN. AUTO-GENERATED. DO NOT REMOVE. --> 38 + 39 + <!-- ANDROID FS PLUGIN. AUTO-GENERATED. DO NOT REMOVE. --> 37 40 </manifest>
+46 -3
mobile/src-tauri/src/lib.rs
··· 21 21 .show(|_| {}); 22 22 } 23 23 24 + // you shoud use async fn 25 + #[tauri::command] 26 + #[specta::specta] 27 + async fn open_file_persistent_android(app: tauri::AppHandle) -> Result<Option<String>, String> { 28 + if cfg!(not(target_os = "android")) { 29 + panic!("Persistent save dialog cannot be called on this platform"); 30 + } 31 + 32 + use tauri_plugin_android_fs::AndroidFsExt; 33 + let api = app.android_fs_async(); 34 + 35 + let selected_file = api 36 + .file_picker() 37 + .pick_file( 38 + None, // Initial location 39 + &["application/json"], // Target MIME types 40 + false, // If true, only files on local device 41 + ) 42 + .await; 43 + let selected_file = match selected_file { 44 + Ok(Some(file)) => file, 45 + Ok(None) => return Ok(None), 46 + Err(err) => return Err(format!("Error picking file: {err}")), 47 + }; 48 + 49 + // Persist file access between restarts 50 + match api 51 + .file_picker() 52 + .persist_uri_permission(&selected_file) 53 + .await 54 + { 55 + Ok(_) => {} 56 + Err(err) => return Err(format!("Error persisting URI: {err}")), 57 + }; 58 + 59 + Ok(Some(selected_file.uri.to_string())) 60 + } 61 + 24 62 fn load_library_from_file(library_json: &str) -> anyhow::Result<Library> { 25 63 let now = Instant::now(); 26 64 ··· 67 105 } 68 106 69 107 pub fn gen_types() -> Builder { 70 - let specta_builder = tauri_specta::Builder::<tauri::Wry>::new() 71 - .commands(tauri_specta::collect_commands![error_popup, load_library]); 108 + let specta_builder = 109 + tauri_specta::Builder::<tauri::Wry>::new().commands(tauri_specta::collect_commands![ 110 + error_popup, 111 + load_library, 112 + open_file_persistent_android 113 + ]); 72 114 73 115 #[cfg(all(debug_assertions, not(target_os = "android")))] 74 - #[cfg(debug_assertions)] 75 116 specta_builder 76 117 .export( 77 118 specta_typescript::Typescript::default() ··· 97 138 let specta_builder = gen_types(); 98 139 99 140 tauri::Builder::default() 141 + .plugin(tauri_plugin_os::init()) 142 + .plugin(tauri_plugin_android_fs::init()) 100 143 .plugin(tauri_plugin_store::Builder::new().build()) 101 144 .plugin(tauri_plugin_fs::init()) 102 145 .plugin(tauri_plugin_dialog::init())
+15 -4
mobile/src/routes/+page.svelte
··· 5 5 import { readTextFile } from '@tauri-apps/plugin-fs' 6 6 import Library from './Library.svelte' 7 7 import { Store } from '@tauri-apps/plugin-store' 8 + import { platform } from '@tauri-apps/plugin-os' 8 9 import '@saurl/tauri-plugin-safe-area-insets-css-api' 9 10 10 11 let loading = $state(false) ··· 19 20 } 20 21 21 22 async function open_library() { 22 - const path = await open({ 23 - filters: [{ name: 'JSON', extensions: ['json'] }], 24 - }) 23 + let path: string | null = null 24 + if (platform() === 'android') { 25 + const result = await commands.openFilePersistentAndroid() 26 + if (result.status === 'ok') { 27 + path = result.data 28 + } else { 29 + error = result.error 30 + } 31 + } else { 32 + path = await open({ 33 + filters: [{ name: 'JSON', extensions: ['json'] }], 34 + }) 35 + } 25 36 if (path) { 26 37 load_library(path) 27 38 } ··· 41 52 error = result.error 42 53 } 43 54 } catch (e) { 44 - error = e instanceof Error ? e.message : 'Failed to load library' 55 + error = e instanceof Error ? e.message : 'Failed to load library. ' + String(e) 45 56 } finally { 46 57 loading = false 47 58 }