diff --git a/Journal.App/package.json b/Journal.App/package.json index 78b0b3a..6e88438 100644 --- a/Journal.App/package.json +++ b/Journal.App/package.json @@ -18,7 +18,8 @@ "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.6.0", - "@tauri-apps/plugin-opener": "^2" + "@tauri-apps/plugin-opener": "^2", + "tauri-plugin-mic-recorder-api": "^2.0.0" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.6", diff --git a/Journal.App/scripts/tauri-prebuild.mjs b/Journal.App/scripts/tauri-prebuild.mjs index 1c92fcc..170ec2b 100644 --- a/Journal.App/scripts/tauri-prebuild.mjs +++ b/Journal.App/scripts/tauri-prebuild.mjs @@ -41,47 +41,57 @@ function sidecarFileName() { : "Journal.Sidecar"; } +function publishProject(projectPath, runtime) { + const publishArgs = [ + "publish", + projectPath, + "-c", + "Release", + "-r", + runtime, + "--self-contained", + "-p:PublishSingleFile=true", + "-p:IncludeNativeLibrariesForSelfExtract=true", + "-p:IncludeAllContentForSelfExtract=true", + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false", + "-o", + publishOutputDir, + ]; + + const publish = spawnSync("dotnet", publishArgs, { + cwd: repoRoot, + stdio: "inherit", + }); + + if (publish.error) { + throw publish.error; + } + if (publish.status !== 0) { + process.exit(publish.status ?? 1); + } +} + +function stageBinary(fileName) { + const publishedBinary = path.join(publishOutputDir, fileName); + const bundledBinary = path.join(tauriBinDir, fileName); + + if (!existsSync(publishedBinary)) { + throw new Error(`Published binary not found: ${publishedBinary}`); + } + + mkdirSync(tauriBinDir, { recursive: true }); + copyFileSync(publishedBinary, bundledBinary); + console.log(`Staged binary for Tauri: ${bundledBinary}`); +} + const runtime = runtimeForCurrentPlatform(); const sidecarName = sidecarFileName(); -const publishedSidecar = path.join(publishOutputDir, sidecarName); -const bundledSidecar = path.join(tauriBinDir, sidecarName); console.log( `Publishing sidecar for ${process.platform}/${process.arch} (${runtime})...`, ); -const publishArgs = [ - "publish", - sidecarProject, - "-c", - "Release", - "-r", - runtime, - "--self-contained", - "-p:PublishSingleFile=true", - "-p:IncludeNativeLibrariesForSelfExtract=true", - "-p:RestoreIgnoreFailedSources=true", - "-p:NuGetAudit=false", - "-o", - publishOutputDir, -]; - -const publish = spawnSync("dotnet", publishArgs, { - cwd: repoRoot, - stdio: "inherit", -}); - -if (publish.error) { - throw publish.error; -} -if (publish.status !== 0) { - process.exit(publish.status ?? 1); -} - -if (!existsSync(publishedSidecar)) { - throw new Error(`Published sidecar not found: ${publishedSidecar}`); -} - -mkdirSync(tauriBinDir, { recursive: true }); -copyFileSync(publishedSidecar, bundledSidecar); -console.log(`Staged sidecar for Tauri: ${bundledSidecar}`); +console.log("Publishing Journal.Sidecar..."); +publishProject(sidecarProject, runtime); +stageBinary(sidecarName); diff --git a/Journal.App/src-tauri/Cargo.lock b/Journal.App/src-tauri/Cargo.lock index 2191152..95b410b 100644 --- a/Journal.App/src-tauri/Cargo.lock +++ b/Journal.App/src-tauri/Cargo.lock @@ -32,6 +32,28 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.11.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -41,6 +63,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -225,6 +297,24 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -393,6 +483,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -402,6 +494,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -436,11 +537,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -516,6 +676,49 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -631,6 +834,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "deranged" version = "0.5.8" @@ -773,6 +982,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "embed-resource" version = "3.0.6" @@ -1409,6 +1624,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "html5ever" version = "0.29.1" @@ -1719,6 +1940,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1770,6 +2006,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "journalapp" version = "0.1.0" @@ -1779,6 +2025,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-mic-recorder", "tauri-plugin-opener", "tokio", ] @@ -1870,7 +2117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -1890,6 +2137,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.12" @@ -1933,6 +2190,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -1985,6 +2251,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2027,6 +2299,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2036,7 +2322,7 @@ dependencies = [ "bitflags 2.11.0", "jni-sys", "log", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -2048,6 +2334,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2069,12 +2364,33 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2318,12 +2634,41 @@ dependencies = [ "objc2-security", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "open" version = "5.3.3" @@ -2967,6 +3312,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3322,7 +3673,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", "js-sys", - "ndk", + "ndk 0.9.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3486,9 +3837,9 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3499,7 +3850,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3570,7 +3921,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -3693,6 +4044,22 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-mic-recorder" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceccca393df4f90abba52602125d526c71ddd0557b03ac7dd9c3f818325b6d95" +dependencies = [ + "chrono", + "clap", + "cpal", + "hound", + "serde", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -3711,7 +4078,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", + "windows 0.61.3", "zbus", ] @@ -3737,7 +4104,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -3763,7 +4130,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -4268,6 +4635,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.21.0" @@ -4529,7 +4902,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -4553,7 +4926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -4603,6 +4976,16 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -4625,6 +5008,16 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4706,6 +5099,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -5125,7 +5527,7 @@ dependencies = [ "jni", "kuchikiki", "libc", - "ndk", + "ndk 0.9.0", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -5143,7 +5545,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/Journal.App/src-tauri/Cargo.toml b/Journal.App/src-tauri/Cargo.toml index ccc2244..1f39d74 100644 --- a/Journal.App/src-tauri/Cargo.toml +++ b/Journal.App/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ name = "journalapp" version = "0.1.0" description = "A Tauri App" -authors = ["Stan"] +authors = ["Stan", "J. Schmidt"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,4 +23,5 @@ tauri-plugin-dialog = "2" tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", features = ["process", "io-util", "sync"] } +tokio = { version = "1", features = ["process", "io-util", "sync", "time"] } +tauri-plugin-mic-recorder = "2.0.0" diff --git a/Journal.App/src-tauri/capabilities/default.json b/Journal.App/src-tauri/capabilities/default.json index cce481c..275d58e 100644 --- a/Journal.App/src-tauri/capabilities/default.json +++ b/Journal.App/src-tauri/capabilities/default.json @@ -3,5 +3,10 @@ "identifier": "default", "description": "Capability for the main window", "windows": ["main"], - "permissions": ["core:default", "dialog:default", "opener:default"] + "permissions": [ + "core:default", + "dialog:default", + "opener:default", + "mic-recorder:default" + ] } diff --git a/Journal.App/src-tauri/src/lib.rs b/Journal.App/src-tauri/src/lib.rs index ba633de..34626ec 100644 --- a/Journal.App/src-tauri/src/lib.rs +++ b/Journal.App/src-tauri/src/lib.rs @@ -5,7 +5,7 @@ use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::Stdio; -use tauri::Manager; +use tauri::{Emitter, Manager}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::{Child, ChildStdin, ChildStdout, Command}; use tokio::sync::Mutex; @@ -109,6 +109,10 @@ struct ManagedSidecar { stdout: BufReader, } +struct ManagedSpeechProcess { + poll_task: tokio::task::JoinHandle<()>, +} + impl ManagedSidecar { fn start(root: &Path, resource_dir: Option<&Path>) -> Result { let sidecar_path = resolve_sidecar_path(root, resource_dir)?; @@ -182,8 +186,15 @@ impl Drop for ManagedSidecar { fn drop(&mut self) {} } +impl ManagedSpeechProcess { + fn is_running(&self) -> bool { + !self.poll_task.is_finished() + } +} + struct SidecarState { process: Mutex>, + speech_process: Mutex>, root_override: Mutex>, config_path: PathBuf, resource_dir: Option, @@ -237,6 +248,11 @@ fn resolve_sidecar_path(root: &Path, resource_dir: Option<&Path>) -> Result) -> Result Result { + serde_json::from_str::(response_line) + .map_err(|err| format!("Invalid sidecar JSON response: {err}")) +} + +fn read_field<'a>(data: &'a Value, camel: &str, pascal: &str) -> Option<&'a Value> { + data.get(camel).or_else(|| data.get(pascal)) +} + fn find_sidecar_executable(search_root: &Path, exe_name: &str) -> Option { if !search_root.is_dir() { return None; @@ -332,11 +357,52 @@ async fn send_with_managed_sidecar( Err("Failed to send command to sidecar.".to_string()) } +async fn send_sidecar_action( + state: &SidecarState, + action: &str, + payload: Option, +) -> Result { + let envelope = serde_json::json!({ + "action": action, + "payload": payload.unwrap_or_else(|| serde_json::json!({})) + }); + let input_line = serde_json::to_string(&envelope) + .map_err(|err| format!("Serialize command failed: {err}"))?; + let response_line = send_with_managed_sidecar(state, &input_line).await?; + let response = parse_command_response(&response_line)?; + + let ok = response + .get("ok") + .and_then(|node| node.as_bool()) + .unwrap_or(false); + if !ok { + let err = response + .get("error") + .and_then(|node| node.as_str()) + .unwrap_or("Sidecar command failed."); + return Err(err.to_string()); + } + + Ok(response + .get("data") + .cloned() + .unwrap_or_else(|| serde_json::json!({}))) +} + async fn stop_managed_sidecar(state: &SidecarState) { let mut guard = state.process.lock().await; guard.take(); } +async fn stop_speech_process(state: &SidecarState) -> Result<(), String> { + let mut guard = state.speech_process.lock().await; + if let Some(process) = guard.take() { + process.poll_task.abort(); + } + + Ok(()) +} + #[tauri::command] async fn get_sidecar_root(state: tauri::State<'_, SidecarState>) -> Result { let root_override = state.root_override.lock().await.clone(); @@ -425,11 +491,151 @@ async fn shutdown( state: tauri::State<'_, SidecarState>, app_handle: tauri::AppHandle, ) -> Result<(), String> { + stop_speech_process(state.inner()).await?; + let _ = send_sidecar_action(state.inner(), "speech.live.stop", None).await; stop_managed_sidecar(state.inner()).await; app_handle.exit(0); Ok(()) } +#[tauri::command] +async fn speech_start( + state: tauri::State<'_, SidecarState>, + app_handle: tauri::AppHandle, +) -> Result { + let _ = app_handle.emit( + "speech-status", + serde_json::json!({ "state": "starting", "message": "Starting speech process..." }), + ); + + { + let guard = state.speech_process.lock().await; + if let Some(existing) = guard.as_ref() { + if existing.is_running() { + return Ok(serde_json::json!({ "running": true })); + } + } + } + + let start_data = send_sidecar_action(state.inner(), "speech.live.start", None).await?; + let running = read_field(&start_data, "running", "Running") + .and_then(|node| node.as_bool()) + .unwrap_or(false); + let status = read_field(&start_data, "status", "Status") + .and_then(|node| node.as_str()) + .unwrap_or("starting"); + let warning = read_field(&start_data, "warning", "Warning") + .and_then(|node| node.as_str()) + .map(|v| v.to_string()); + + let _ = app_handle.emit( + "speech-status", + serde_json::json!({ "state": status, "message": warning.clone().unwrap_or_else(|| status.to_string()) }), + ); + + if !running { + return Err(warning.unwrap_or_else(|| "Failed to start live speech.".to_string())); + } + + let app_for_poll = app_handle.clone(); + let poll_task = tokio::spawn(async move { + loop { + let state_handle = app_for_poll.state::(); + let poll_data = match send_sidecar_action( + state_handle.inner(), + "speech.live.poll", + Some(serde_json::json!({ "maxItems": 8 })), + ) + .await + { + Ok(value) => value, + Err(err) => { + let _ = app_for_poll.emit( + "speech-status", + serde_json::json!({ "state": "error", "message": err }), + ); + break; + } + }; + + if let Some(items) = read_field(&poll_data, "items", "Items").and_then(|node| node.as_array()) { + for item in items { + if let Some(text) = item.as_str() { + let _ = app_for_poll + .emit("speech-transcript", serde_json::json!({ "text": text })); + } + } + } + + let running = read_field(&poll_data, "running", "Running") + .and_then(|node| node.as_bool()) + .unwrap_or(false); + let status = read_field(&poll_data, "status", "Status") + .and_then(|node| node.as_str()) + .unwrap_or(if running { "listening" } else { "stopped" }); + let warning = read_field(&poll_data, "warning", "Warning") + .and_then(|node| node.as_str()) + .map(|v| v.to_string()); + if let Some(message) = warning { + let _ = app_for_poll.emit( + "speech-status", + serde_json::json!({ "state": if running { "listening" } else { "error" }, "message": message }), + ); + } else { + let _ = app_for_poll.emit( + "speech-status", + serde_json::json!({ "state": status, "message": status }), + ); + } + + if !running { + break; + } + + tokio::time::sleep(std::time::Duration::from_millis(350)).await; + } + }); + + let mut guard = state.speech_process.lock().await; + *guard = Some(ManagedSpeechProcess { poll_task }); + Ok(serde_json::json!({ "running": true })) +} + +#[tauri::command] +async fn speech_stop( + state: tauri::State<'_, SidecarState>, + app_handle: tauri::AppHandle, +) -> Result { + stop_speech_process(state.inner()).await?; + let _ = send_sidecar_action(state.inner(), "speech.live.stop", None).await; + let _ = app_handle.emit( + "speech-status", + serde_json::json!({ "state": "stopped", "message": "Dictation stopped." }), + ); + Ok(serde_json::json!({ "running": false })) +} + +#[tauri::command] +async fn speech_cleanup_probe(path: String) -> Result { + if path.trim().is_empty() { + return Ok(serde_json::json!({ "deleted": false })); + } + + let target = PathBuf::from(path); + let normalized = target.to_string_lossy().to_lowercase(); + if !normalized.contains("tauri-plugin-mic-recorder") || !normalized.ends_with(".wav") { + return Ok(serde_json::json!({ "deleted": false })); + } + + if !target.exists() { + return Ok(serde_json::json!({ "deleted": false })); + } + + fs::remove_file(&target).map_err(|err| format!("Failed to remove probe recording: {err}"))?; + + Ok(serde_json::json!({ "deleted": true })) +} + #[tauri::command] async fn sidecar_command( state: tauri::State<'_, SidecarState>, @@ -442,18 +648,21 @@ async fn sidecar_command( let input_line = serde_json::to_string(&command) .map_err(|err| format!("Serialize command failed: {err}"))?; let response_line = send_with_managed_sidecar(state.inner(), &input_line).await?; - serde_json::from_str::(&response_line) - .map_err(|err| format!("Invalid sidecar JSON response: {err}")) + parse_command_response(&response_line) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let app = tauri::Builder::default() + .plugin(tauri_plugin_mic_recorder::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ sidecar_command, shutdown, + speech_start, + speech_stop, + speech_cleanup_probe, get_sidecar_root, set_sidecar_root, get_ui_settings, @@ -468,6 +677,7 @@ pub fn run() { app.manage(SidecarState { process: Mutex::new(None), + speech_process: Mutex::new(None), root_override: Mutex::new(root_override), config_path, resource_dir: app.path().resource_dir().ok(), @@ -483,6 +693,11 @@ pub fn run() { if let Ok(mut guard) = state.process.try_lock() { guard.take(); }; + if let Ok(mut guard) = state.speech_process.try_lock() { + if let Some(speech) = guard.take() { + speech.poll_task.abort(); + } + }; } }); } diff --git a/Journal.App/src-tauri/tauri.conf.json b/Journal.App/src-tauri/tauri.conf.json index 6506fa3..ad09961 100644 --- a/Journal.App/src-tauri/tauri.conf.json +++ b/Journal.App/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Project Journal", "version": "0.1.0", - "identifier": "com.stan.journal", + "identifier": "com.idsolutions.journal", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", diff --git a/Journal.App/src/lib/backend/speech.ts b/Journal.App/src/lib/backend/speech.ts new file mode 100644 index 0000000..9dd975a --- /dev/null +++ b/Journal.App/src/lib/backend/speech.ts @@ -0,0 +1,33 @@ +import { invoke } from "$lib/runtime/invoke"; +import { + startRecording as startMicRecording, + stopRecording as stopMicRecording, +} from "tauri-plugin-mic-recorder-api"; + +type SpeechControlResult = { + running: boolean; + pid?: number; + launch?: string; +}; + +export async function startSpeechDictation(): Promise { + return invoke("speech_start"); +} + +export async function stopSpeechDictation(): Promise { + return invoke("speech_stop"); +} + +export async function probeMicrophoneAccess(): Promise { + await startMicRecording(); + await new Promise((resolve) => setTimeout(resolve, 300)); + const outputPath = await stopMicRecording(); + try { + await invoke<{ deleted: boolean }>("speech_cleanup_probe", { + path: outputPath, + }); + } catch { + // Keep probe non-blocking; cleanup failure should not break dictation start. + } + return outputPath; +} diff --git a/Journal.App/src/lib/components/editor/FragmentEditor.svelte b/Journal.App/src/lib/components/editor/FragmentEditor.svelte index ba34034..7987604 100644 --- a/Journal.App/src/lib/components/editor/FragmentEditor.svelte +++ b/Journal.App/src/lib/components/editor/FragmentEditor.svelte @@ -1,5 +1,11 @@
+ {#if dictationError || dictationStatus} +
+ {dictationError || dictationStatus} +
+ {/if} {#if fragmentMode === "view"}
{@html renderMarkdown(openDocumentContent)} @@ -297,6 +469,17 @@ aria-label="Fragment body" >
+
+