tauri-app: game review, with video showcase, stats and clip export
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m6s

This commit is contained in:
2026-03-27 20:43:20 +01:00
parent 1e979d3ec4
commit c516ad6b13
7 changed files with 2317 additions and 97 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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<String> {
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<String> {
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<String, String> {
// 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<String, String> {
// 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<String, String> {
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<Value, String> {
// 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");

View File

@@ -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": {

View File

@@ -1,9 +1,38 @@
<script setup lang="ts">
import { ref } from "vue";
import GameHistory from "./components/GameHistory.vue";
import GameReview from "./components/GameReview.vue";
import type { GameHistoryItem } from "./types/timeline";
// Current view state
const currentView = ref<"history" | "review">("history");
const selectedGame = ref<GameHistoryItem | null>(null);
// Navigate to review view
function openReview(game: GameHistoryItem) {
selectedGame.value = game;
currentView.value = "review";
}
// Navigate back to history
function closeReview() {
currentView.value = "history";
selectedGame.value = null;
}
</script>
<template>
<GameHistory />
<div class="app">
<GameHistory
v-if="currentView === 'history'"
@open-review="openReview"
/>
<GameReview
v-else-if="currentView === 'review' && selectedGame"
:game="selectedGame"
@back="closeReview"
/>
</div>
</template>
<style>
@@ -38,4 +67,9 @@ body {
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.app {
min-height: 100vh;
background: #0a0a13;
}
</style>

View File

@@ -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(() => {
<img
v-if="getSummonerSpells(game)"
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell1Id)"
:alt="getSummonerSpells(game)!.spell1Name || 'Spell 1'"
alt="Spell 1"
class="spell-image"
/>
<div v-else class="spell-placeholder"></div>
@@ -146,7 +156,7 @@ onMounted(() => {
<img
v-if="getSummonerSpells(game)"
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell2Id)"
:alt="getSummonerSpells(game)!.spell2Name || 'Spell 2'"
alt="Spell 2"
class="spell-image"
/>
<div v-else class="spell-placeholder"></div>
@@ -348,8 +358,8 @@ onMounted(() => {
<div class="modal-actions">
<button class="btn-secondary" @click="closeDetail">Close</button>
<button class="btn-primary">
Open Video
<button class="btn-primary" @click="openReview(selectedGame)">
Review Game
</button>
</div>
</div>

File diff suppressed because it is too large Load Diff