From c516ad6b13830cdbf644810a1272bdb1a02362c3 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 27 Mar 2026 20:43:20 +0100 Subject: [PATCH] tauri-app: game review, with video showcase, stats and clip export --- tauri-app/src-tauri/Cargo.lock | 345 +++-- tauri-app/src-tauri/Cargo.toml | 5 +- tauri-app/src-tauri/src/lib.rs | 321 ++++- tauri-app/src-tauri/tauri.conf.json | 18 +- tauri-app/src/App.vue | 36 +- tauri-app/src/components/GameHistory.vue | 18 +- tauri-app/src/components/GameReview.vue | 1671 ++++++++++++++++++++++ 7 files changed, 2317 insertions(+), 97 deletions(-) create mode 100644 tauri-app/src/components/GameReview.vue diff --git a/tauri-app/src-tauri/Cargo.lock b/tauri-app/src-tauri/Cargo.lock index 8fb7ab0..065ee5b 100644 --- a/tauri-app/src-tauri/Cargo.lock +++ b/tauri-app/src-tauri/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -670,6 +679,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -716,11 +736,11 @@ dependencies = [ [[package]] name = "directories" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys 0.4.1", + "dirs-sys", ] [[package]] @@ -729,19 +749,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", + "dirs-sys", ] [[package]] @@ -752,7 +760,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.2", ] @@ -963,6 +971,19 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ffmpeg-sidecar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f076483fb6efcf02e4abcf3e9388d30123346f85b9a96e8fe834718951b945ed" +dependencies = [ + "anyhow", + "tar", + "ureq", + "xz2", + "zip", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -973,6 +994,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -987,6 +1019,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1550,6 +1583,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -1973,7 +2012,10 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags 2.11.0", "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -2003,6 +2045,17 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac" version = "0.1.1" @@ -2399,7 +2452,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2632,6 +2685,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -2907,14 +2966,12 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "redox_syscall" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", + "bitflags 2.11.0", ] [[package]] @@ -3011,6 +3068,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3039,6 +3110,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3407,7 +3513,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -3501,6 +3607,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3616,6 +3728,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3639,6 +3762,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", @@ -3679,6 +3803,7 @@ version = "0.1.0" dependencies = [ "chrono", "directories", + "ffmpeg-sidecar", "serde", "serde_json", "tauri", @@ -4322,6 +4447,41 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -4353,6 +4513,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4624,6 +4790,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4856,11 +5031,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -4905,21 +5080,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4977,12 +5137,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5001,12 +5155,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5025,12 +5173,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5061,12 +5203,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5085,12 +5221,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5109,12 +5239,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5133,12 +5257,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5347,6 +5465,25 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.8.1" @@ -5472,6 +5609,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" @@ -5505,12 +5648,44 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.13.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zvariant" version = "5.10.0" diff --git a/tauri-app/src-tauri/Cargo.toml b/tauri-app/src-tauri/Cargo.toml index b5b749e..6081a55 100644 --- a/tauri-app/src-tauri/Cargo.toml +++ b/tauri-app/src-tauri/Cargo.toml @@ -18,11 +18,12 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["protocol-asset"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["v4", "serde"] } -directories = "5" +directories = "6" +ffmpeg-sidecar = "2.4" diff --git a/tauri-app/src-tauri/src/lib.rs b/tauri-app/src-tauri/src/lib.rs index 5b8e448..11ed0a7 100644 --- a/tauri-app/src-tauri/src/lib.rs +++ b/tauri-app/src-tauri/src/lib.rs @@ -1,3 +1,9 @@ +use ffmpeg_sidecar::{ + command::{ffmpeg_is_installed, FfmpegCommand}, + download::auto_download, + event::{FfmpegEvent, LogLevel}, + paths::sidecar_path, +}; use serde_json::Value; use std::fs; use std::path::PathBuf; @@ -77,6 +83,312 @@ fn get_recordings_dir() -> String { .unwrap_or_else(|| "./recordings".to_string()) } +/// Find a video file in the recordings directory. +/// Searches for exact filename match or pattern match. +#[tauri::command] +fn find_video_file(recordings_dir: String, filename: String) -> Option { + let recordings_path = PathBuf::from(&recordings_dir); + + if !recordings_path.exists() { + return None; + } + + // Try exact match first + let exact_path = recordings_path.join(&filename); + if exact_path.exists() { + return Some(exact_path.to_string_lossy().to_string()); + } + + // Try to find by pattern (date_time_*.mp4) + if let Ok(entries) = fs::read_dir(&recordings_path) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().map(|e| e == "mp4" || e == "mkv" || e == "mov").unwrap_or(false) { + let file_name = path.file_name()?.to_string_lossy().to_string(); + + // Check if filename starts with the same date/time pattern + // filename format: "2026-03-27_16-42-52_unknown.mp4" + // We match the date_time prefix + if file_name.starts_with(&filename.replace("_unknown.mp4", "")) { + return Some(path.to_string_lossy().to_string()); + } + } + } + } + + None +} + +/// Get list of video files in recordings directory. +#[tauri::command] +fn list_video_files(recordings_dir: String) -> Vec { + let recordings_path = PathBuf::from(&recordings_dir); + let mut videos = Vec::new(); + + if !recordings_path.exists() { + return videos; + } + + if let Ok(entries) = fs::read_dir(&recordings_path) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().map(|e| e == "mp4" || e == "mkv" || e == "mov").unwrap_or(false) { + videos.push(path.to_string_lossy().to_string()); + } + } + } + + // Sort by modification time, newest first + videos.sort_by(|a, b| { + let a_time = fs::metadata(a).and_then(|m| m.modified()).ok(); + let b_time = fs::metadata(b).and_then(|m| m.modified()).ok(); + b_time.cmp(&a_time) + }); + + videos +} + +/// Ensure ffmpeg is available, downloading if necessary. +fn ensure_ffmpeg() -> Result<(), String> { + if ffmpeg_is_installed() { + return Ok(()); + } + + // Auto-download ffmpeg for the current platform + auto_download().map_err(|e| format!("Failed to download ffmpeg: {}", e))?; + + Ok(()) +} + +/// Export a clip from a video using ffmpeg-sidecar (stream copy for speed). +#[tauri::command] +fn export_clip( + video_path: String, + start_time: f64, + end_time: f64, + output_name: String, +) -> Result { + // Ensure ffmpeg is available + ensure_ffmpeg()?; + + // Verify input file exists + if !PathBuf::from(&video_path).exists() { + return Err(format!("Video file not found: {}", video_path)); + } + + // Get output directory + let output_dir = get_default_output_dir() + .map(|p| p.join("clips")) + .unwrap_or_else(|| PathBuf::from("./clips")); + + // Create clips directory if it doesn't exist + if !output_dir.exists() { + fs::create_dir_all(&output_dir) + .map_err(|e| format!("Failed to create clips directory: {}", e))?; + } + + let output_path = output_dir.join(format!("{}.mp4", output_name)); + let duration = end_time - start_time; + + // Build ffmpeg command with stream copy (fast, no re-encoding) + // Note: Put -ss before -i for fast seeking (input seeking) + let iter = FfmpegCommand::new() + .arg("-y") // Overwrite output file + .arg("-ss").arg(&format!("{:.3}", start_time)) + .arg("-i").arg(&video_path) + .arg("-t").arg(&format!("{:.3}", duration)) + .arg("-c").arg("copy") // Stream copy for fast export + .arg("-avoid_negative_ts").arg("make_zero") + .arg(&output_path.to_string_lossy().to_string()) + .spawn() + .map_err(|e| format!("Failed to spawn ffmpeg: {}", e))? + .iter() + .map_err(|e| format!("Failed to create ffmpeg iterator: {}", e))?; + + // Process the ffmpeg command and collect all errors + let mut errors = Vec::new(); + for event in iter { + match event { + FfmpegEvent::Error(e) => { + errors.push(format!("FFmpeg error: {}", e)); + } + FfmpegEvent::Log(LogLevel::Error, msg) => { + errors.push(format!("FFmpeg log error: {}", msg)); + } + FfmpegEvent::Log(LogLevel::Warning, msg) => { + // Log warnings but don't fail + eprintln!("FFmpeg warning: {}", msg); + } + _ => {} + } + } + + // Check if output file was created + if !output_path.exists() { + return Err(format!( + "Export failed - output file not created. Errors: {}", + errors.join("; ") + )); + } + + // Check if output file has content + let metadata = fs::metadata(&output_path) + .map_err(|e| format!("Failed to check output file: {}", e))?; + if metadata.len() == 0 { + return Err(format!( + "Export failed - output file is empty. Errors: {}", + errors.join("; ") + )); + } + + Ok(output_path.to_string_lossy().to_string()) +} + +/// Export a clip with re-encoding for more precise cuts. +#[tauri::command] +fn export_clip_precise( + video_path: String, + start_time: f64, + end_time: f64, + output_name: String, + quality: String, +) -> Result { + // Ensure ffmpeg is available + ensure_ffmpeg()?; + + // Verify input file exists + if !PathBuf::from(&video_path).exists() { + return Err(format!("Video file not found: {}", video_path)); + } + + let output_dir = get_default_output_dir() + .map(|p| p.join("clips")) + .unwrap_or_else(|| PathBuf::from("./clips")); + + if !output_dir.exists() { + fs::create_dir_all(&output_dir) + .map_err(|e| format!("Failed to create clips directory: {}", e))?; + } + + let output_path = output_dir.join(format!("{}.mp4", output_name)); + let duration = end_time - start_time; + + // Quality presets (CRF values - lower is better quality) + let crf = match quality.as_str() { + "low" => "28", + "medium" => "23", + "high" => "18", + _ => "23", + }; + + // Build ffmpeg command with re-encoding + let iter = FfmpegCommand::new() + .arg("-y") // Overwrite output file + .arg("-ss").arg(&format!("{:.3}", start_time)) + .arg("-i").arg(&video_path) + .arg("-t").arg(&format!("{:.3}", duration)) + .arg("-c:v").arg("libx264") + .arg("-crf").arg(crf) + .arg("-preset").arg("fast") + .arg("-c:a").arg("aac") + .arg("-b:a").arg("128k") + .arg(&output_path.to_string_lossy().to_string()) + .spawn() + .map_err(|e| format!("Failed to spawn ffmpeg: {}", e))? + .iter() + .map_err(|e| format!("Failed to create ffmpeg iterator: {}", e))?; + + // Process the ffmpeg command and collect all errors + let mut errors = Vec::new(); + for event in iter { + match event { + FfmpegEvent::Error(e) => { + errors.push(format!("FFmpeg error: {}", e)); + } + FfmpegEvent::Log(LogLevel::Error, msg) => { + errors.push(format!("FFmpeg log error: {}", msg)); + } + FfmpegEvent::Log(LogLevel::Warning, msg) => { + eprintln!("FFmpeg warning: {}", msg); + } + _ => {} + } + } + + // Check if output file was created + if !output_path.exists() { + return Err(format!( + "Export failed - output file not created. Errors: {}", + errors.join("; ") + )); + } + + // Check if output file has content + let metadata = fs::metadata(&output_path) + .map_err(|e| format!("Failed to check output file: {}", e))?; + if metadata.len() == 0 { + return Err(format!( + "Export failed - output file is empty. Errors: {}", + errors.join("; ") + )); + } + + Ok(output_path.to_string_lossy().to_string()) +} + +/// Check if ffmpeg is available. +#[tauri::command] +fn check_ffmpeg() -> bool { + ffmpeg_is_installed() +} + +/// Download ffmpeg if not already installed. +#[tauri::command] +fn download_ffmpeg() -> Result { + ensure_ffmpeg()?; + Ok(sidecar_path().unwrap_or_default().to_string_lossy().to_string()) +} + +/// Get video file metadata using ffprobe. +#[tauri::command] +fn get_video_metadata(video_path: String) -> Result { + // Ensure ffmpeg is available + ensure_ffmpeg()?; + + // Use ffprobe via ffmpeg-sidecar + let iter = FfmpegCommand::new() + .arg("-v").arg("quiet") + .arg("-print_format").arg("json") + .arg("-show_format") + .arg("-show_streams") + .arg(&video_path) + .spawn() + .map_err(|e| format!("Failed to spawn ffprobe: {}", e))? + .iter() + .map_err(|e| format!("Failed to create ffprobe iterator: {}", e))?; + + let mut json_output = String::new(); + + for event in iter { + match event { + FfmpegEvent::Log(LogLevel::Info, msg) | FfmpegEvent::Log(LogLevel::Unknown, msg) => { + // Capture JSON output + if msg.trim().starts_with('{') || msg.trim().starts_with('[') { + json_output.push_str(&msg); + } + } + FfmpegEvent::Error(e) => { + return Err(format!("FFprobe error: {}", e)); + } + _ => {} + } + } + + // Parse the JSON output + serde_json::from_str(&json_output) + .map_err(|e| format!("Failed to parse ffprobe output: {}", e)) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -84,7 +396,14 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ get_game_history, get_timeline, - get_recordings_dir + get_recordings_dir, + find_video_file, + list_video_files, + export_clip, + export_clip_precise, + check_ffmpeg, + download_ffmpeg, + get_video_metadata ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/tauri-app/src-tauri/tauri.conf.json b/tauri-app/src-tauri/tauri.conf.json index c4cbfbb..dcf121e 100644 --- a/tauri-app/src-tauri/tauri.conf.json +++ b/tauri-app/src-tauri/tauri.conf.json @@ -12,13 +12,23 @@ "app": { "windows": [ { - "title": "tauri-app", - "width": 800, - "height": 600 + "title": "League Recorder", + "width": 1280, + "height": 800, + "minWidth": 800, + "minHeight": 600 } ], "security": { - "csp": null + "csp": null, + "assetProtocol": { + "enable": true, + "scope": [ + "$APPDATA/**", + "$APPDATA/../**", + "C:/Users/**/AppData/Roaming/**" + ] + } } }, "bundle": { diff --git a/tauri-app/src/App.vue b/tauri-app/src/App.vue index b7f8bce..98710ba 100644 --- a/tauri-app/src/App.vue +++ b/tauri-app/src/App.vue @@ -1,9 +1,38 @@ diff --git a/tauri-app/src/components/GameHistory.vue b/tauri-app/src/components/GameHistory.vue index e70be84..8181ff2 100644 --- a/tauri-app/src/components/GameHistory.vue +++ b/tauri-app/src/components/GameHistory.vue @@ -2,6 +2,11 @@ import { ref, onMounted } from "vue"; import { invoke } from "@tauri-apps/api/core"; import type { GameHistoryItem, TimestampedEvent, ItemInfo } from "../types/timeline"; + +// Emits +const emit = defineEmits<{ + (e: "open-review", game: GameHistoryItem): void; +}>(); import { getGameResult, formatDuration, @@ -58,6 +63,11 @@ function closeDetail() { selectedGame.value = null; } +// Open review view for a game +function openReview(game: GameHistoryItem) { + emit("open-review", game); +} + // Helper to get items array for display (6 slots + trinket) function getItemsArray(game: GameHistoryItem): (ItemInfo | null)[] { return getItems(game); @@ -137,7 +147,7 @@ onMounted(() => { Spell 1
@@ -146,7 +156,7 @@ onMounted(() => { Spell 2
@@ -348,8 +358,8 @@ onMounted(() => { diff --git a/tauri-app/src/components/GameReview.vue b/tauri-app/src/components/GameReview.vue new file mode 100644 index 0000000..ec3e2af --- /dev/null +++ b/tauri-app/src/components/GameReview.vue @@ -0,0 +1,1671 @@ + + + + +