[improve] set game dir on client -> updates Play/Install button based on games existing

This commit is contained in:
ddidderr 2024-11-14 19:41:55 +01:00
parent c00b7dbe9c
commit 942eb8003e
Signed by: ddidderr
GPG Key ID: 3841F1C27E6F0E14
12 changed files with 873 additions and 53 deletions

570
Cargo.lock generated
View File

@ -163,6 +163,146 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ashpd"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.8.5",
"raw-window-handle",
"serde",
"serde_repr",
"tokio",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "async-broadcast"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-io"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [
"async-lock",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling 3.7.4",
"rustix",
"slab",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "async-lock"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
"tracing",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "async-signal"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "atk"
version = "0.18.0"
@ -342,6 +482,19 @@ dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "borsh"
version = "1.3.0"
@ -940,6 +1093,17 @@ dependencies = [
"serde",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_more"
version = "0.99.18"
@ -1003,6 +1167,15 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.5",
]
[[package]]
name = "dlopen2"
version = "0.7.0"
@ -1032,6 +1205,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dpi"
version = "0.1.1"
@ -1106,6 +1285,33 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "env_filter"
version = "0.1.2"
@ -1164,6 +1370,16 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "eyre"
version = "0.6.12"
@ -1357,6 +1573,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.31"
@ -1755,6 +1984,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "hex"
version = "0.4.3"
@ -2393,8 +2628,10 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-log",
"tauri-plugin-shell",
"tauri-plugin-store",
"tokio",
]
@ -2588,7 +2825,7 @@ dependencies = [
"flume",
"if-addrs",
"log",
"polling",
"polling 2.8.0",
"socket2",
]
@ -2635,7 +2872,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.9",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
@ -2703,6 +2940,18 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@ -2940,6 +3189,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.6.0",
"block2",
"dispatch",
"libc",
"objc2",
]
@ -3081,6 +3331,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.1"
@ -3324,6 +3584,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
@ -3359,7 +3630,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap 2.6.0",
"quick-xml",
"quick-xml 0.32.0",
"serde",
"time",
]
@ -3393,6 +3664,21 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "polling"
version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -3512,6 +3798,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.37"
@ -3724,6 +4019,29 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "rfd"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e"
dependencies = [
"ashpd",
"block2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "ring"
version = "0.17.8"
@ -4089,6 +4407,12 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -4673,6 +4997,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string_cache"
version = "0.8.7"
@ -4992,6 +5322,45 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4307310e1d2c09ab110235834722e7c2b85099b683e1eb7342ab351b0be5ada3"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 1.0.68",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror 1.0.68",
"url",
"uuid",
]
[[package]]
name = "tauri-plugin-log"
version = "2.0.2"
@ -5035,6 +5404,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-store"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a580be53f04bb62422d239aa798e88522877f58a0d4a0e745f030055a51bb4"
dependencies = [
"dunce",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 1.0.68",
"tokio",
]
[[package]]
name = "tauri-runtime"
version = "2.2.0"
@ -5280,6 +5665,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
@ -5484,6 +5870,17 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset",
"tempfile",
"winapi",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
@ -5807,6 +6204,66 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wayland-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"scoped-tls",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [
"bitflags 2.6.0",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e"
dependencies = [
"bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3"
dependencies = [
"proc-macro2",
"quick-xml 0.36.2",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09"
dependencies = [
"dlib",
"log",
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.72"
@ -6375,6 +6832,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xdg-home"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "yoke"
version = "0.7.4"
@ -6399,6 +6866,65 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030"
dependencies = [
"async-broadcast",
"async-process",
"async-recursion",
"async-trait",
"derivative",
"enumflags2",
"event-listener",
"futures-core",
"futures-sink",
"futures-util",
"hex",
"nix",
"ordered-stream",
"rand 0.8.5",
"serde",
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@ -6482,3 +7008,41 @@ dependencies = [
"quote",
"syn 2.0.87",
]
[[package]]
name = "zvariant"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65"
dependencies = [
"endi",
"enumflags2",
"serde",
"static_assertions",
"url",
"zvariant_derive",
]
[[package]]
name = "zvariant_derive"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 1.0.109",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]

View File

@ -1,10 +1,12 @@
#![allow(clippy::missing_errors_doc)]
use std::{net::SocketAddr, time::Duration};
use bytes::{Bytes, BytesMut};
use lanspread_db::db::Game;
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient};
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
@ -21,7 +23,39 @@ pub enum ClientCommand {
ServerAddr(SocketAddr),
}
/// # Errors
async fn initial_server_alive_check(conn: &mut Connection) -> bool {
let stream = match conn.open_bidirectional_stream().await {
Ok(stream) => stream,
Err(e) => {
log::error!("failed to open stream: {e}");
return false;
}
};
let (mut rx, mut tx) = stream.split();
// send ping
if let Err(e) = tx.send(Request::Ping.encode()).await {
log::error!("failed to send ping to server: {e}");
return false;
}
let _ = tx.close().await;
// receive pong
if let Ok(Some(response)) = rx.receive().await {
let response = Response::decode(response);
if let Response::Pong = response {
log::info!("server is alive");
return true;
}
log::error!("server sent invalid response to ping: {response:?}");
}
false
}
#[allow(clippy::too_many_lines)]
pub async fn run(
mut rx_control: UnboundedReceiver<ClientCommand>,
tx_notify_ui: UnboundedSender<ClientEvent>,
@ -58,6 +92,10 @@ pub async fn run(
conn.keep_alive(true)?;
if !initial_server_alive_check(&mut conn).await {
continue;
}
log::info!(
"connected: (server: {}) (client: {})",
maybe_addr!(conn.remote_addr()),
@ -131,6 +169,7 @@ pub async fn run(
String::from_utf8_lossy(&data)
);
}
Response::Pong => (), // ignore (should never happen)
}
if let Err(err) = tx.close().await {

View File

@ -29,6 +29,8 @@ pub struct Game {
pub size: u64,
/// thumbnail image
pub thumbnail: Option<Bytes>,
/// only relevant for client (yeah... I know)
pub installed: bool,
}
impl fmt::Debug for Game {
@ -76,7 +78,7 @@ pub struct GameDB {
impl GameDB {
#[must_use]
pub fn new() -> Self {
pub fn empty() -> Self {
GameDB {
games: HashMap::new(),
}
@ -84,7 +86,7 @@ impl GameDB {
#[must_use]
pub fn from(games: Vec<Game>) -> Self {
let mut db = GameDB::new();
let mut db = GameDB::empty();
for game in games {
db.games.insert(game.id.clone(), game);
}
@ -99,6 +101,14 @@ impl GameDB {
self.games.get(id.as_ref())
}
#[must_use]
pub fn get_mut_game_by_id<S>(&mut self, id: S) -> Option<&mut Game>
where
S: AsRef<str>,
{
self.games.get_mut(id.as_ref())
}
#[must_use]
pub fn get_game_by_name(&self, name: &str) -> Option<&Game> {
self.games.values().find(|game| game.name == name)
@ -110,10 +120,16 @@ impl GameDB {
games.sort_by(|a, b| a.name.cmp(&b.name));
games
}
pub fn set_all_uninstalled(&mut self) {
for game in self.games.values_mut() {
game.installed = false;
}
}
}
impl Default for GameDB {
fn default() -> Self {
Self::new()
Self::empty()
}
}

View File

@ -5,6 +5,7 @@ use tracing::error;
#[derive(Debug, Serialize, Deserialize)]
pub enum Request {
Ping,
ListGames,
GetGame { id: String },
Invalid(Bytes, String),
@ -12,6 +13,7 @@ pub enum Request {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Response {
Pong,
Games(Vec<Game>),
Game(Game),
GameNotFound(String),

View File

@ -116,6 +116,7 @@ impl RequestHandler {
async fn handle_request(&self, request: Request) -> Response {
match request {
Request::Ping => Response::Pong,
Request::ListGames => {
let db = self.db.lock().await;
Response::Games(db.all_games().into_iter().cloned().collect())
@ -172,6 +173,7 @@ fn eti_game_to_game(eti_game: EtiGame) -> Game {
genre: eti_game.genre_de,
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
thumbnail: None,
installed: false,
}
}

View File

@ -3,7 +3,9 @@
"specifiers": {
"npm:@tauri-apps/api@2": "2.1.1",
"npm:@tauri-apps/cli@2": "2.1.0",
"npm:@tauri-apps/plugin-dialog@~2.0.1": "2.0.1",
"npm:@tauri-apps/plugin-shell@2": "2.0.1",
"npm:@tauri-apps/plugin-store@2.1": "2.1.0",
"npm:@types/react-dom@^18.2.7": "18.3.1",
"npm:@types/react@^18.2.15": "18.3.12",
"npm:@vitejs/plugin-react@^4.2.1": "4.3.3_vite@5.4.11_@babel+core@7.26.0",
@ -348,12 +350,24 @@
"@tauri-apps/cli-win32-x64-msvc"
]
},
"@tauri-apps/plugin-dialog@2.0.1": {
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
"dependencies": [
"@tauri-apps/api"
]
},
"@tauri-apps/plugin-shell@2.0.1": {
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
"dependencies": [
"@tauri-apps/api"
]
},
"@tauri-apps/plugin-store@2.1.0": {
"integrity": "sha512-GADqrc17opUKYIAKnGHIUgEeTZ2wJGu1ZITKQ1WMuOFdv8fvXRFBAqsqPjE3opgWohbczX6e1NpwmZK1AnuWVw==",
"dependencies": [
"@tauri-apps/api"
]
},
"@types/babel__core@7.20.5": {
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dependencies": [
@ -604,7 +618,9 @@
"dependencies": [
"npm:@tauri-apps/api@2",
"npm:@tauri-apps/cli@2",
"npm:@tauri-apps/plugin-dialog@~2.0.1",
"npm:@tauri-apps/plugin-shell@2",
"npm:@tauri-apps/plugin-store@2.1",
"npm:@types/react-dom@^18.2.7",
"npm:@types/react@^18.2.15",
"npm:@vitejs/plugin-react@^4.2.1",

View File

@ -10,6 +10,8 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/plugin-dialog": "~2.0.1",
"@tauri-apps/plugin-store": "~2.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@tauri-apps/api": "^2",

View File

@ -32,4 +32,6 @@ tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { workspace = true }
tauri-plugin-dialog = "2"
tauri-plugin-store = "2"

View File

@ -2,9 +2,13 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:default",
"shell:allow-open"
"shell:allow-open",
"dialog:default",
"store:default"
]
}
}

View File

@ -1,8 +1,13 @@
use std::net::SocketAddr;
use std::{
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
use tauri::{AppHandle, Emitter as _, Listener as _, Manager};
use tauri::{AppHandle, Emitter as _, Manager};
use tokio::sync::{mpsc::UnboundedSender, Mutex};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
@ -10,6 +15,7 @@ use tokio::sync::{mpsc::UnboundedSender, Mutex};
struct LanSpreadState {
server_addr: Mutex<Option<SocketAddr>>,
client_ctrl: UnboundedSender<ClientCommand>,
games: Arc<Mutex<GameDB>>,
}
#[tauri::command]
@ -40,6 +46,67 @@ fn run_game_backend(id: String, state: tauri::State<LanSpreadState>) -> String {
// }
}
fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) {
if let Some(file_name) = path.file_name() {
if let Some(file_name) = file_name.to_str() {
if let Some(game) = game_db.get_mut_game_by_id(file_name) {
if installed {
log::error!("Game is installed: {game}");
} else {
log::error!("Game is missing: {game}");
}
game.installed = installed;
}
}
}
}
#[tauri::command]
fn update_game_directory(app_handle: tauri::AppHandle, path: String) {
let path = PathBuf::from(path);
if !path.exists() {
log::error!("game dir {path:?} does not exist");
}
let entries = match path.read_dir() {
Ok(entries) => entries,
Err(e) => {
log::error!("Failed to read game dir: {e}");
return;
}
};
tauri::async_runtime::spawn(async move {
let mut game_db = app_handle
.state::<LanSpreadState>()
.inner()
.games
.lock()
.await;
// Reset all games to uninstalled
game_db.set_all_uninstalled();
// update game_db with installed games from real game directory
entries.into_iter().for_each(|entry| {
if let Ok(entry) = entry {
if let Ok(path_type) = entry.file_type() {
if path_type.is_dir() {
let path = entry.path();
if path.join(".softlan_game_installed").exists() {
set_game_install_state_from_path(&mut game_db, &path, true);
}
}
}
}
});
if let Err(e) = app_handle.emit("games-list-updated", Some(game_db.all_games())) {
log::error!("Failed to emit games-list-updated event: {e}");
}
});
}
async fn find_server(app: AppHandle) {
log::info!("Looking for server...");
@ -57,6 +124,7 @@ async fn find_server(app: AppHandle) {
.client_ctrl
.send(ClientCommand::ServerAddr(server_addr))
.unwrap();
request_games(state);
break;
}
Err(e) => {
@ -66,6 +134,25 @@ async fn find_server(app: AppHandle) {
}
}
async fn update_game_db(games: Vec<Game>, app: AppHandle) {
for game in &games {
log::trace!("client event ListGames iter: {game:?}");
}
let state = app.state::<LanSpreadState>();
// Store games list
let mut state_games = state.games.lock().await;
*state_games = GameDB::from(games.clone());
// Tell Frontend about new games list
if let Err(e) = app.emit("games-list-updated", Some(games)) {
log::error!("Failed to emit games-list-updated event: {e}");
} else {
log::info!("Emitted games-list-updated event");
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let tauri_logger_builder = tauri_plugin_log::Builder::new()
@ -89,12 +176,19 @@ pub fn run() {
let lanspread_state = LanSpreadState {
server_addr: Mutex::new(None),
client_ctrl: tx_client_control,
games: Arc::new(Mutex::new(GameDB::empty())),
};
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_logger_builder.build())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![run_game_backend, request_games])
.invoke_handler(tauri::generate_handler![
run_game_backend,
request_games,
update_game_directory
])
.manage(lanspread_state)
.setup(|app| {
let app_handle = app.handle().clone();
@ -110,16 +204,7 @@ pub fn run() {
match event {
ClientEvent::ListGames(games) => {
log::debug!("Received client event: ListGames");
for game in &games {
log::trace!("client event ListGames iter: {game:?}");
}
if let Err(e) = app_handle.emit("games-list-updated", Some(games)) {
log::error!("Failed to emit games-list-updated event: {e}");
} else {
log::info!("Emitted games-list-updated event");
}
update_game_db(games, app_handle.clone()).await;
}
}
}

View File

@ -102,13 +102,6 @@ h1.align-center {
box-shadow: 0 8px 15px rgba(0, 191, 255, 0.2);
}
/* .play-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
/* box-shadow: 0 15px 20px rgba(0, 191, 255, 0.4); */
/* box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6), 0 0 15px rgba(0, 191, 255, 0.5);
transform: translateY(-5px);
} */
.play-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6);
@ -124,7 +117,6 @@ h1.align-center {
}
.search-container {
padding: 20px;
display: flex;
justify-content: center;
}
@ -150,3 +142,45 @@ h1.align-center {
.search-input::placeholder {
color: #8892b0;
}
.search-settings-wrapper {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 20px;
}
.settings-container {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 15px;
}
.settings-button {
padding: 8px 16px;
background: linear-gradient(45deg, #09305a, #37529c);
color: #D5DBFE;
border: 1px solid transparent;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 191, 255, 0.2);
}
.settings-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
border: 1px solid rgba(0, 191, 255, 0.6);
transform: translateY(-2px);
}
.settings-text {
color: #8892b0;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}

View File

@ -1,40 +1,78 @@
import {useEffect, useState} from 'react';
import {invoke} from '@tauri-apps/api/core';
import {listen} from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog';
import { load } from '@tauri-apps/plugin-store';
import "./App.css";
const FILE_STORAGE = 'launcher-settings.json';
const GAME_DIR_KEY = 'game-directory';
interface Game {
id: string;
name: string;
description: string;
size: number;
thumbnail: Uint8Array;
installed: boolean;
}
const App = () => {
const [gameItems, setGameItems] = useState<Game[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [gameDir, setGameDir] = useState('');
const filteredGames = gameItems.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const getInitialGameDir = async () => {
// update game directory from storage (if exists)
// only if it's not already set
await new Promise(resolve => setTimeout(resolve, 1000));
const store = await load(FILE_STORAGE, { autoSave: true });
const savedGameDir = await store.get<string>(GAME_DIR_KEY);
if (savedGameDir) {
setGameDir(savedGameDir);
}
};
useEffect(() => {
if (gameDir) {
// store game directory in persistent storage
const updateStorage = async (game_dir: string) => {
try {
const store = await load(FILE_STORAGE, { autoSave: true });
await store.set(GAME_DIR_KEY, game_dir);
console.info(`📦 Storage updated with game directory: ${game_dir}`);
} catch (error) {
console.error('❌ Error updating storage:', error);
}
};
updateStorage(gameDir);
console.log(`📂 Game directory changed to: ${gameDir}`);
invoke('update_game_directory', { path: gameDir })
.catch(error => console.error('❌ Error updating game directory:', error));
}
}, [gameDir]);
useEffect(() => {
console.log('🔵 Effect starting - setting up listener and requesting games');
let isSubscribed = true; // For tracking if component is still mounted
const setupEventListener = async () => {
try {
const unlisten = await listen('games-list-updated', (event) => {
if (!isSubscribed) return; // Don't update state if unmounted
console.log('📥 Received games-list-updated event');
// Listen for events that update the game list
const unlisten_games = await listen('games-list-updated', (event) => {
console.log('🗲 Received games-list-updated event');
const games = event.payload as Game[];
games.forEach(game => {
console.log(`🎮 game: ${JSON.stringify(game.id)}`);
});
console.log(`🎮 ${games.length} Games received`);
setGameItems(games);
getInitialGameDir();
});
// Initial request for games
@ -44,8 +82,7 @@ const App = () => {
// Cleanup function
return () => {
console.log('🧹 Cleaning up - removing listener');
isSubscribed = false;
unlisten();
unlisten_games();
};
} catch (error) {
console.error('❌ Error in setup:', error);
@ -57,7 +94,6 @@ const App = () => {
// Cleanup
return () => {
console.log('🚫 Effect cleanup - component unmounting');
isSubscribed = false;
};
}, []); // Empty dependency array means this runs once on mount
@ -71,20 +107,38 @@ const App = () => {
}
};
const dialogGameDir = async () => {
const file = await open({
multiple: false,
directory: true,
});
if (file) {
setGameDir(file);
}
};
// Rest of your component remains the same
return (
<main className="container">
<div className="fixed-header">
<h1 className="align-center">SoftLAN Launcher</h1>
<div className="main-header">
<div className="search-container">
<input
type="text"
placeholder="Search games..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<div className="search-settings-wrapper">
<div></div>
<div className="search-container">
<input
type="text"
placeholder="Search games..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="settings-container">
<button onClick={dialogGameDir} className="settings-button">Set Game Directory</button>
<span className="settings-text">{gameDir}</span>
</div>
</div>
</div>
</div>
@ -101,7 +155,7 @@ const App = () => {
<span className="desc-text">{item.description.slice(0, 10)}</span>
<span className="size-text">{item.size.toString()}</span>
</div>
<div className="play-button">Play</div>
<div className="play-button">{item.installed ? 'Play' : 'Install'}</div>
</div>
);
})}