Compare commits

..

38 Commits

Author SHA1 Message Date
ffb05e3a0a [clippy] fix clippy issues 2025-06-25 21:18:36 +02:00
8fda4f79ed [code] Mutex -> RwLock 2025-06-25 21:16:39 +02:00
f16d8b1dca [deps] cargo update
Adding   match_token         v0.1.0
Removing itoa                v0.4.8
Removing thin-slice          v0.1.1
Updating brotli-decompressor v4.0.3  -> v5.0.0
Updating brotli              v7.0.0  -> v8.0.1
Updating bumpalo             v3.18.1 -> v3.19.0
Updating cssparser           v0.27.2 -> v0.29.6
Updating html5ever           v0.26.0 -> v0.29.1
Updating kuchikiki           v0.8.2  -> v0.8.8-speedreader
Updating libredox            v0.1.3  -> v0.1.4
Updating markup5ever         v0.11.0 -> v0.14.1
Updating mdns-sd             v0.13.9 -> v0.13.10
Updating num_enum_derive     v0.7.3  -> v0.7.4
Updating num_enum            v0.7.3  -> v0.7.4
Updating phf_codegen         v0.10.0 -> v0.11.3
Updating phf_macros          v0.8.0  -> v0.10.0
Updating selectors           v0.22.0 -> v0.24.0
Updating servo_arc           v0.1.1  -> v0.2.0
Updating tao                 v0.33.0 -> v0.34.0
Updating tauri-build         v2.2.0  -> v2.3.0
Updating tauri-codegen       v2.2.0  -> v2.3.0
Updating tauri-macros        v2.2.0  -> v2.3.0
Updating tauri-plugin-dialog v2.2.2  -> v2.3.0
Updating tauri-plugin-fs     v2.3.0  -> v2.4.0
Updating tauri-plugin-log    v2.5.0  -> v2.6.0
Updating tauri-plugin-shell  v2.2.2  -> v2.3.0
Updating tauri-plugin-store  v2.2.1  -> v2.3.0
Updating tauri-plugin        v2.2.0  -> v2.3.0
Updating tauri-runtime       v2.6.0  -> v2.7.0
Updating tauri-runtime-wry   v2.6.0  -> v2.7.0
Updating tauri-utils         v2.4.0  -> v2.5.0
Updating tauri               v2.5.1  -> v2.6.0
Updating webview2-com-sys    v0.37.0 -> v0.38.0
Updating webview2-com        v0.37.0 -> v0.38.0
Updating wry                 v0.51.2 -> v0.52.0
2025-06-25 20:42:40 +02:00
7566c2908f [deps] cargo update
Adding   dispatch2                v0.3.0
Adding   getrandom                v0.2.16
Adding   getrandom                v0.3.3
Adding   icu_locale_core          v2.0.0
Adding   iri-string               v0.7.8
Adding   once_cell_polyfill       v1.70.1
Adding   potential_utf            v0.1.2
Adding   ref-cast-impl            v1.0.24
Adding   ref-cast                 v1.0.24
Adding   schemars                 v0.9.0
Adding   sigchld                  v0.2.3
Adding   signal-hook              v0.3.18
Adding   toml_write               v0.1.2
Adding   tower-http               v0.6.6
Adding   windows-core             v0.61.2
Adding   windows-future           v0.2.1
Adding   windows-strings          v0.4.2
Adding   windows-targets          v0.53.2
Adding   windows-threading        v0.1.0
Adding   windows                  v0.61.3
Adding   zerocopy-derive          v0.8.26
Adding   zerocopy                 v0.8.26
Adding   zerotrie                 v0.2.2
Removing getrandom                v0.2.15
Removing getrandom                v0.3.2
Removing icu_locid_transform_data v1.5.0
Removing icu_locid_transform      v1.5.0
Removing icu_locid                v1.5.0
Removing icu_provider_macros      v1.5.0
Removing utf16_iter               v1.0.5
Removing windows_aarch64_gnullvm  v0.48.5
Removing windows_aarch64_msvc     v0.48.5
Removing windows-collections      v0.1.1
Removing windows-core             v0.52.0
Removing windows-core             v0.60.1
Removing windows-core             v0.61.0
Removing windows-future           v0.1.1
Removing windows-future           v0.2.0
Removing windows_i686_gnu         v0.48.5
Removing windows_i686_msvc        v0.48.5
Removing windows-implement        v0.59.0
Removing windows-numerics         v0.1.1
Removing windows-registry         v0.4.0
Removing windows-strings          v0.3.1
Removing windows-strings          v0.4.0
Removing windows-targets          v0.48.5
Removing windows-targets          v0.53.0
Removing windows                  v0.60.0
Removing windows                  v0.61.1
Removing windows_x86_64_gnullvm   v0.48.5
Removing windows_x86_64_gnu       v0.48.5
Removing windows_x86_64_msvc      v0.48.5
Removing write16                  v1.0.0
Removing xdg-home                 v1.3.0
Removing zerocopy-derive          v0.7.35
Removing zerocopy-derive          v0.8.24
Removing zerocopy                 v0.7.35
Removing zerocopy                 v0.8.24
Updating adler2                   v2.0.0                         -> v2.0.1
Updating anstream                 v0.6.18                        -> v0.6.19
Updating anstyle-parse            v0.2.6                         -> v0.2.7
Updating anstyle-query            v1.1.2                         -> v1.1.3
Updating anstyle                  v1.0.10                        -> v1.0.11
Updating anstyle-wincon           v3.0.7                         -> v3.0.9
Updating anyhow                   v1.0.97                        -> v1.0.98
Updating autocfg                  v1.4.0                         -> v1.5.0
Updating aws-lc-rs                v1.12.6                        -> v1.13.1
Updating aws-lc-sys               v0.27.1                        -> v0.29.0
Updating backtrace                v0.3.74                        -> v0.3.75
Updating bitflags                 v2.9.0                         -> v2.9.1
Updating block2                   v0.6.0                         -> v0.6.1
Updating borsh-derive             v1.5.6                         -> v1.5.7
Updating borsh                    v1.5.6                         -> v1.5.7
Updating brotli-decompressor      v4.0.2                         -> v4.0.3
Updating bumpalo                  v3.17.0                        -> v3.18.1
Updating bytemuck                 v1.22.0                        -> v1.23.1
Updating camino                   v1.1.9                         -> v1.1.10
Updating cc                       v1.2.17                        -> v1.2.27
Updating cfg-if                   v1.0.0                         -> v1.0.1
Updating chrono                   v0.4.40                        -> v0.4.41
Updating clap_builder             v4.5.32                        -> v4.5.40
Updating clap_derive              v4.5.32                        -> v4.5.40
Updating clap_lex                 v0.7.4                         -> v0.7.5
Updating clap                     v4.5.32                        -> v4.5.40
Updating colorchoice              v1.0.3                         -> v1.0.4
Updating core-foundation          v0.10.0                        -> v0.10.1
Updating crc                      v3.2.1                         -> v3.3.0
Updating crossbeam-channel        v0.5.14                        -> v0.5.15
Updating darling_core             v0.20.10                       -> v0.20.11
Updating darling_macro            v0.20.10                       -> v0.20.11
Updating darling                  v0.20.10                       -> v0.20.11
Updating derive_more              v0.99.19                       -> v0.99.20
Updating dlopen2_derive           v0.4.0                         -> v0.4.1
Updating dpi                      v0.1.1                         -> v0.1.2
Updating embed-resource           v3.0.2                         -> v3.0.4
Updating enumflags2_derive        v0.7.11                        -> v0.7.12
Updating enumflags2               v0.7.11                        -> v0.7.12
Updating errno                    v0.3.10                        -> v0.3.13
Updating event-listener-strategy  v0.5.3                         -> v0.5.4
Updating flate2                   v1.1.0                         -> v1.1.2
Updating gethostname              v1.0.0                         -> v1.0.2
Updating hashbrown                v0.15.2                        -> v0.15.4
Updating hash_hasher              v2.0.3                         -> v2.0.4
Updating hyper-util               v0.1.10                        -> v0.1.14
Updating iana-time-zone           v0.1.61                        -> v0.1.63
Updating icu_collections          v1.5.0                         -> v2.0.0
Updating icu_normalizer_data      v1.5.0                         -> v2.0.0
Updating icu_normalizer           v1.5.0                         -> v2.0.0
Updating icu_properties_data      v1.5.0                         -> v2.0.1
Updating icu_properties           v1.5.1                         -> v2.0.1
Updating icu_provider             v1.5.0                         -> v2.0.0
Updating idna_adapter             v1.2.0                         -> v1.2.1
Updating indexmap                 v2.8.0                         -> v2.9.0
Updating jobserver                v0.1.32                        -> v0.1.33
Updating libc                     v0.2.171                       -> v0.2.174
Updating libloading               v0.8.6                         -> v0.8.8
Updating libm                     v0.2.11                        -> v0.2.15
Updating linux-raw-sys            v0.9.3                         -> v0.9.4
Updating litemap                  v0.7.5                         -> v0.8.0
Updating lock_api                 v0.4.12                        -> v0.4.13
Updating log                      v0.4.26                        -> v0.4.27
Updating mdns-sd                  v0.13.3                        -> v0.13.9
Updating memchr                   v2.7.4                         -> v2.7.5
Updating miniz_oxide              v0.8.5                         -> v0.8.9
Updating mio                      v1.0.3                         -> v1.0.4
Updating nix                      v0.29.0                        -> v0.30.1
Updating objc2-app-kit            v0.3.0                         -> v0.3.1
Updating objc2-cloud-kit          v0.3.0                         -> v0.3.1
Updating objc2-core-data          v0.3.0                         -> v0.3.1
Updating objc2-core-foundation    v0.3.0                         -> v0.3.1
Updating objc2-core-graphics      v0.3.0                         -> v0.3.1
Updating objc2-core-image         v0.3.0                         -> v0.3.1
Updating objc2-foundation         v0.3.0                         -> v0.3.1
Updating objc2-io-surface         v0.3.0                         -> v0.3.1
Updating objc2-quartz-core        v0.3.0                         -> v0.3.1
Updating objc2-ui-kit             v0.3.0                         -> v0.3.1
Updating objc2                    v0.6.0                         -> v0.6.1
Updating objc2-web-kit            v0.3.0                         -> v0.3.1
Updating once_cell                v1.21.1                        -> v1.21.3
Updating os_pipe                  v1.2.1                         -> v1.2.2
Updating parking_lot_core         v0.9.10                        -> v0.9.11
Updating parking_lot              v0.12.3                        -> v0.12.4
Updating plist                    v1.7.0                         -> v1.7.2
Updating prettyplease             v0.2.31                        -> v0.2.35
Updating proc-macro2              v1.0.94                        -> v1.0.95
Updating quick-xml                v0.32.0                        -> v0.37.5
Updating rand                     v0.9.0                         -> v0.9.1
Updating redox_syscall            v0.5.10                        -> v0.5.13
Updating r-efi                    v5.2.0                         -> v5.3.0
Updating reqwest                  v0.12.15                       -> v0.12.20
Updating rustc-demangle           v0.1.24                        -> v0.1.25
Updating rust_decimal             v1.37.0                        -> v1.37.2
Updating rustix                   v1.0.3                         -> v1.0.7
Updating rustls-pki-types         v1.11.0                        -> v1.12.0
Updating rustls                   v0.23.25                       -> v0.23.28
Updating rustls-webpki            v0.103.0                       -> v0.103.3
Updating rustversion              v1.0.20                        -> v1.0.21
Updating s2n-codec                v0.55.0                        -> v0.59.0
Updating s2n-quic-core            v0.55.0                        -> v0.59.0
Updating s2n-quic-crypto          v0.55.0                        -> v0.59.0
Updating s2n-quic-platform        v0.55.0                        -> v0.59.0
Updating s2n-quic-rustls          v0.55.0                        -> v0.59.0
Updating s2n-quic-tls-default     v0.55.0                        -> v0.59.0
Updating s2n-quic-tls             v0.55.0                        -> v0.59.0
Updating s2n-quic-transport       v0.55.0                        -> v0.59.0
Updating s2n-quic                 v1.55.0                        -> v1.59.0
Updating s2n-tls-sys              v0.3.14                        -> v0.3.21
Updating s2n-tls                  v0.3.14                        -> v0.3.21
Updating serde_spanned            v0.6.8                         -> v0.6.9
Updating serde_with_macros        v3.12.0                        -> v3.13.0
Updating serde_with               v3.12.0                        -> v3.13.0
Updating sha2                     v0.10.8                        -> v0.10.9
Updating shared_child             v1.0.1                         -> v1.1.0
Updating signal-hook-registry     v1.4.2                         -> v1.4.5
Updating slab                     v0.4.9                         -> v0.4.10
Updating smallvec                 v1.14.0                        -> v1.15.1
Updating socket2                  v0.5.8                         -> v0.5.10
Updating sqlx-core                v0.8.3                         -> v0.8.6
Updating sqlx-macros-core         v0.8.3                         -> v0.8.6
Updating sqlx-macros              v0.8.3                         -> v0.8.6
Updating sqlx-sqlite              v0.8.3                         -> v0.8.6
Updating sqlx                     v0.8.3                         -> v0.8.6
Updating string_cache             v0.8.8                         -> v0.8.9
Updating synstructure             v0.13.1                        -> v0.13.2
Updating syn                      v2.0.100                       -> v2.0.104
Updating tao                      v0.32.8                        -> v0.33.0
Updating tauri-build              v2.1.0                         -> v2.2.0
Updating tauri-codegen            v2.1.0                         -> v2.2.0
Updating tauri-macros             v2.1.0                         -> v2.2.0
Updating tauri-plugin-dialog      v2.2.0                         -> v2.2.2
Updating tauri-plugin-fs          v2.2.0                         -> v2.3.0
Updating tauri-plugin-log         v2.3.1                         -> v2.5.0
Updating tauri-plugin-shell       v2.2.0                         -> v2.2.2
Updating tauri-plugin-store       v2.2.0                         -> v2.2.1
Updating tauri-plugin             v2.1.0                         -> v2.2.0
Updating tauri-runtime            v2.5.0                         -> v2.6.0
Updating tauri-runtime-wry        v2.5.0                         -> v2.6.0
Updating tauri-utils              v2.3.0                         -> v2.4.0
Updating tauri                    v2.4.0                         -> v2.5.1
Updating tauri-winres             v0.3.0                         -> v0.3.1
Updating tempfile                 v3.19.1                        -> v3.20.0
Updating thread_local             v1.1.8                         -> v1.1.9
Updating time-macros              v0.2.21                        -> v0.2.22
Updating time                     v0.3.40                        -> v0.3.41
Updating tinystr                  v0.7.6                         -> v0.8.1
Updating tokio-util               v0.7.14                        -> v0.7.15
Updating tokio                    v1.44.1                        -> v1.45.1
Updating toml_datetime            v0.6.8                         -> v0.6.11
Updating toml_edit                v0.22.24                       -> v0.22.27
Updating toml                     v0.8.20                        -> v0.8.23
Updating tracing-attributes       v0.1.28                        -> v0.1.30
Updating tracing-core             v0.1.33                        -> v0.1.34
Updating tray-icon                v0.20.0                        -> v0.20.1
Updating uuid                     v1.16.0                        -> v1.17.0
Updating value-bag                v1.10.0                        -> v1.11.1
Updating wasi                     v0.11.0+wasi-snapshot-preview1 -> v0.11.1+wasi-snapshot-preview1
Updating webview2-com-sys         v0.36.0                        -> v0.37.0
Updating webview2-com             v0.36.0                        -> v0.37.0
Updating windows-link             v0.1.1                         -> v0.1.3
Updating windows-result           v0.3.2                         -> v0.3.4
Updating windows-sys              v0.48.0                        -> v0.60.2
Updating winnow                   v0.7.4                         -> v0.7.11
Updating winreg                   v0.52.0                        -> v0.55.0
Updating writeable                v0.5.5                         -> v0.6.1
Updating wry                      v0.50.5                        -> v0.51.2
Updating yoke-derive              v0.7.5                         -> v0.8.0
Updating yoke                     v0.7.5                         -> v0.8.0
Updating zbus_macros              v5.5.0                         -> v5.7.1
Updating zbus                     v5.5.0                         -> v5.7.1
Updating zerovec-derive           v0.10.3                        -> v0.11.1
Updating zerovec                  v0.10.4                        -> v0.11.2
Updating zvariant_derive          v5.4.0                         -> v5.5.3
Updating zvariant                 v5.4.0                         -> v5.5.3
2025-06-21 17:47:45 +02:00
89ec65f0d2 [assets] unrar binary for aarch64-apple-darwin 2025-03-22 14:08:52 +01:00
bad6baa9de [deps] cargo update 2025-03-22 13:56:12 +01:00
4369090a53 [debug] change spammy mDNS from debug to trace 2025-03-22 13:55:05 +01:00
1ef5e4d01a [fix] non-windows systems 2025-03-21 10:54:29 +01:00
572beb66f7 [readme] improved 2025-03-21 08:19:49 +01:00
366b6fbca7 [client] no cmd window when running games or scripts 2025-03-20 23:02:09 +01:00
d80dece8a7 [deps] cargo update 2025-03-20 23:01:49 +01:00
d69cf115c8 [client] run as admin 2025-03-20 22:34:34 +01:00
78f7ff2405 [wip] use windows crate to run as admin 2025-03-20 20:57:32 +01:00
393f8b5fab [frontend][deps] update frontend dependencies and note in README on how to do that 2025-03-20 19:44:35 +01:00
765447e6d1 [code][fix] improvements for LAN 202503
- more robust client <-> server connection
  - new client event: DownloadGameFilesFailed
  - 3 seconds to reconnect
  - retry forever if server is gone and never lose a UI request

- code cleanup here and there (mostly server)
2025-03-20 19:39:32 +01:00
19434cd1b1 [deps] cargo update (and a few unused crates for future stuff) 2025-03-20 19:38:35 +01:00
b8bedbdfab [README][dev] improved README, adjusted scripts 2025-03-20 19:38:00 +01:00
fda97f53be [deps] frontend update 2025-03-04 09:34:46 +01:00
f28ecc9f8b [code] RwLock instead of Mutex makes more sense for the GameDB 2025-03-03 17:33:09 +01:00
45a4b9218f [dev] add game.db and thumbnails to gitignore 2025-03-02 14:42:16 +01:00
7dd200ec54 [dev] server.sh simplified 2025-03-02 14:42:05 +01:00
0b381ee198 [certs] update certificates 2025-03-02 14:41:08 +01:00
adf6f9d757 [code] improve structure (focus: server) 2025-03-02 13:06:18 +01:00
bcf9ad68ad [deps] cargo update 2025-03-02 13:05:35 +01:00
b21091c247 [code] edition 2024 2025-03-02 13:05:01 +01:00
d1eb185498 [deps] move all deps into main toml and update 2024-12-03 20:49:13 +01:00
56c1eb0167 [ui] wait for server, rename to softlan-launcher 2024-11-15 12:29:16 +01:00
1dd25f682b [server] only provide games that exist in games folder 2024-11-15 12:15:50 +01:00
3610eb77a6 [ui] show size in GB 2024-11-15 12:10:02 +01:00
89e3565806 [fix] remove warning 2024-11-15 11:56:22 +01:00
dcee3d55b2 [client] run game 2024-11-15 11:53:25 +01:00
bc70d6300b [client] unpack game works! 2024-11-15 11:20:35 +01:00
f9cd8471b4 [backup] on the way 2024-11-15 08:59:53 +01:00
2b64d1e4ba [client][server] file transfer working, ui not ready for it 2024-11-14 23:26:31 +01:00
942eb8003e [improve] set game dir on client -> updates Play/Install button based on games existing 2024-11-14 19:41:55 +01:00
c00b7dbe9c [code] remove server struct 2024-11-14 16:04:56 +01:00
a2a630893f [ui] better default windows size 2024-11-14 00:43:33 +01:00
0eb332c14d [logging] make message logging clearer 2024-11-14 00:42:54 +01:00
37 changed files with 3549 additions and 2327 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target /target
/game.db
/thumbnails/

3160
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,17 +12,39 @@ members = [
resolver = "2" resolver = "2"
[workspace.dependencies] [workspace.dependencies]
bytes = { version = "1.8", features = ["serde"] } bytes = { version = "1", features = ["serde"] }
clap = { version = "4.5", features = ["derive"] } chrono = "0.4"
clap = { version = "4", features = ["derive"] }
eyre = "0.6" eyre = "0.6"
itertools = "0.13" gethostname = "1"
s2n-quic = { version = "1.49", features = ["provider-event-tracing"] } itertools = "0.14"
semver = "1.0" log = "0.4"
serde = { version = "1.0", features = ["derive"] } mdns-sd = "0.13"
serde_json = "1.0" s2n-quic = { version = "1", features = ["provider-event-tracing"] }
tokio = { version = "1.41", features = ["full"] } semver = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", default-features = false, features = [
"derive",
"runtime-tokio",
"sqlite",
] }
tauri = { version = "2", features = [] }
tauri-plugin-log = "2"
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-store = "2"
tokio = { version = "1", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["v7"] }
walkdir = "2"
windows = { version = "0.61", features = [
"Win32",
"Win32_UI",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
[profile.release] [profile.release]
debug = true debug = true

View File

@ -10,33 +10,36 @@ Simple server and GUI for LAN parties.
### Prerequisites ### Prerequisites
```bash ```bash
# install Tauri CLI # install Tauri CLI
cargo install tauri-cli --version "^2.0.0" cargo install tauri-cli
# install trunk (Build, bundle & ship your Rust WASM application to the web) # install Deno with a package manager or from https://deno.land/
cargo install trunk
# alternatively if you have problems (i.e. on Windows)
cargo install cargo-binstall
cargo binstall trunk
# install Rust WASM target
rustup target add wasm32-unknown-unknown
``` ```
### Build ### Build
#### Frontend #### Frontend
```bash ```bash
# Development # Development
cargo tauri dev # prefix with RUST_LOG=your_module=debug or similary for more verbose output cargo tauri dev # prefix with RUST_LOG=your_module=debug or similary for more verbose output
# Production but for testing and without bundling
cargo tauri build --no-bundle
# Production # Production
cargo tauri build --profile release-lto # also bundles everything into a nice platform-specific installer cargo tauri build -- --profile release-lto # also bundles everything into a nice platform-specific installer
# on wayland you probably need to set this env var
WEBKIT_DISABLE_DMABUF_RENDERER=1
# update frontend dependencies
deno outdated --update --latest
``` ```
#### Backend #### Backend
```bash ```bash
# Development # Development
./server.sh # prefix with RUST_LOG=your_module=debug or similary for more verbose output ./server.sh [options...] # prefix with RUST_LOG=your_module=debug or similary for more verbose output
# Production # Production
cargo build --profile release-lto cargo build --profile release-lto -p lanspread-server
``` ```

View File

@ -1,35 +1,35 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIGCTCCA/GgAwIBAgIUJKXDdTgaLqvRC86wusXKd1tX+q4wDQYJKoZIhvcNAQEL MIIGCTCCA/GgAwIBAgIUDpGo26xcNO0Pw/L8axkDSVCnupcwDQYJKoZIhvcNAQEL
BQAwgZUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl BQAwgZUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl
cmxpbjESMBAGA1UECgwJUGF1bG9zb2Z0MREwDwYDVQQLDAhTb2Z0d2FyZTEkMCIG cmxpbjESMBAGA1UECgwJUGF1bG9zb2Z0MREwDwYDVQQLDAhTb2Z0d2FyZTEkMCIG
CSqGSIb3DQEJARYVZGRpZGRlcnJAcGF1bC5uZXR3b3JrMRcwFQYDVQQDDA5NYXN0 CSqGSIb3DQEJARYVZGRpZGRlcnJAcGF1bC5uZXR3b3JrMRcwFQYDVQQDDA5NYXN0
ZXJEZXNhc3RlcjAeFw0yNDAyMDMxNDI4MzNaFw0yNTAyMDIxNDI4MzNaMIGVMQsw ZXJEZXNhc3RlcjAeFw0yNTAzMDIxMjM0MzRaFw0yNjAzMDIxMjM0MzRaMIGVMQsw
CQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEjAQ CQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEjAQ
BgNVBAoMCVBhdWxvc29mdDERMA8GA1UECwwIU29mdHdhcmUxJDAiBgkqhkiG9w0B BgNVBAoMCVBhdWxvc29mdDERMA8GA1UECwwIU29mdHdhcmUxJDAiBgkqhkiG9w0B
CQEWFWRkaWRkZXJyQHBhdWwubmV0d29yazEXMBUGA1UEAwwOTWFzdGVyRGVzYXN0 CQEWFWRkaWRkZXJyQHBhdWwubmV0d29yazEXMBUGA1UEAwwOTWFzdGVyRGVzYXN0
ZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQClHI7EI1cAO40AOooG ZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0DbMTTEoUKT9ZNLuR
FxqCxw7f3yjv4yGkEjx5Xk5zyURxrspMG1MbFfHGVlhnpe0vTMzMFwrrl7FeQ2+T DKGG84TLu5XauGJiQPoJhnIinSyC/ods0pvmjjcxrrsw39tt6JFb8yz2S6LX3wwL
wmypwqrGZs009DP/CFFG8BX7EBX2SKQzFzUdWWYTwHtPtw8RHD+Aee0UA8QH9Y/o bwgOPhV3BLzDZYqzbMidequSppsodcKNnZ901SwiNrF2OK1ZXORuUoM4AJXdz5qJ
koKYysja1ahyOmbk7lg7B+igTZKipXFyigX/H3iK1n2gJU8gBlQcRfyI8VTYfTjA ZTLyiKOUwQB3obME1cA1zIrBhHiRfXtFTwV+dnwb6L696Nz1z/vxmfzkO95vNppp
k9hllluzycOuKSAQXrVCArbQfTUl2edd4t0tmvjear0wV2PclVm+8AXnsSKkg3Cj 0gpz7qd9NoKqOHMSPCIhxrVcw31IGscUWGqEbEiK1Q12YHqxTpHs7ITB4xGbiWdR
+0mdDvTKVxkycCTJwGa7iMe3OchFaGqGwRBRnPr6lbqNjmI0CwK+cNcwxEDcSos8 ERwZoV3SyAAg+YgHLtGrKOGgxw/E+L6eDwIC2NUjq+hlORWbUfV8i71fOMvZS0eA
UtyQ6amCpLS0fhsLL1SyPqvj8toGRr0cSaM4suMx24Pl7WXItCdW/dijteqe/p41 7hUIDJopZ/2xEYHCo1793HY9rLDAo2AippvAMG/BmTi2nnrpb2QOFFCNh/qOn+VU
BTtMTNXBjsCl26oTE8FIQHbxzFhVkKVGV6UyHrkmdjyGmdwLqt5+ks2eaVrW3Pld wGDNAqrpNsNgk5MQQBtAjeyG4mzkK8sCYI3H+YVEZ6YMA7GkYBqfiPiFCtdop1TE
yw0h0kWe2KmK+A/95QpZA7KNlYrkEi2M1cZZXmiQZh3uOWBb9P94zQ262tz+jStQ IVgo5RkAl4LpbRO03bm65QV5GEKgsWoz7wgGxaYmnjNPoHa4bdEXZe+3W2J6VNba
hkFfHSmFqXSOgyRc7VA/6CkR/qStWWwMcgLJcjnmQ0ixtaW6zrObHZBD+72Q3lHm i9sXwf/E9A6axNDjy1o0fLkK/IFomk0zHzsVxy4HzBBR/8IqEz8bRt/Qy1QbBP9I
0m11mc55tP44UadNY8jjlLWrL3xq4txMyqvwWaE0xIdzec0Oj5Gu/aPivym3BYY5 0xNallACSHtM2AdsrAkNCrcpaNdrQ14J2cGn5fvRcxKar5bzrDwHcX2pn76wdMMz
AiGHO4P7x53k3zr118hsMQi4WQIDAQABo08wTTAJBgNVHRMEAjAAMAsGA1UdDwQE 1b3io+FolYI/aFeqb9hPX5V+7wIDAQABo08wTTAJBgNVHRMEAjAAMAsGA1UdDwQE
AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFFg0Y/xYk6IdBXDN AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNX3XPuVanpmZ73j
cSOIoHxrNiecMA0GCSqGSIb3DQEBCwUAA4ICAQBjvz66og+q1n9mwN7vxuolosJV btjaeqiALr6UMA0GCSqGSIb3DQEBCwUAA4ICAQAGxfcR8y9/jCudqpCpOthHW01+
99x9kg/sf0RTdwkAVuphZrwIoEXskGywsRmzAdnOZXCWf5pO14l/eC782dEDQA7M 12hlPa5CYgRpXpr5n2E/O87+Cq/day7S8gcXKiDBCyhw19HFl8O0b0D0vHfwZFu3
+0ap4i8iMUZMFAFbAK52wLvS5tCwuNnm+zqPa1tjyfs4VVGmO9jIQk5+P97NRJzY aN4uj86moMROde1CJT59u86+D0EwjInDS126BeKtxovqWh1RPcOdHp4oB8F5IxCT
vBT676SVZLnOs5DD2FdOCbZrPgvuOlfpcdQIE5ufxJ7en2RObYe8VPjnQb7zp4WE UiNXqgj9nsL7GoU4yqdQ3TvtJKU8EbsglnBK9/FxeTg5xp38H4FWcwcI5oGVLKBb
RClK10xSjv6onwpyDZu7t9Sk9JtnH14jMPeRWw58SPYrcvElpvwvlewlCsWriWgX JvRI7YluoiYh934K+HMZI+mSpZut5Y5+QSwqy5xMyCbZ6AkVE4CUCGDMn1aUvRNl
Mb/1ME1ZajvOGwfMnanDM2uBRjb2pdvOogpLEhgSK4UsH9MaQleHzez32qK5Xl// sO1vE9aDkn9++Kp0XDNHoPJXZZ/c5EVLdWcmuVzErLWkonfqTHgZ8kbdeBMrTsNt
5adttoTr7P48vGJsSNRcG1taPGE2Mv+qHVw7IX2Md6ekWy6L/lTLhH/nbf5QNQTU TaZ0gIQDf+W0idOxhJLIawqv8GK14PuG/a8LlIh37qS3sHHy5aY7lCxI/Ui+Hxhr
QANCKrsYQNbneF598mXAX3Z4nkwVyCpXFwzOSEaqHkuuQclnCZhNb8HrB6U5c4aj Mnjf7roDhjotgUtHsMQGxmXsCMJmVFW6NjtX4b1ZVQRtxionpNo/fzdqJRuaLt5E
MX7IPaRS7qS27rXcn6WskvPehERb6nhZiz8YiDqdX0ZhaDqflvhOIqzVmx6AY5we uOktXbMPbbRdCyUzgGdxX/iz55osvBdJQCcDVdjiq6QEjo+vdBJTr4QDMQs4Q1p0
FvEwLfSmVzw0hE/R3DgH0MH8QTUZSLe0gQwyZmqq+3gJv4R7l7DbmObKQneO68Kq OtZswBn34f9O38YlJRLemGhP0ZT0XRTyPJOVU0V1eBRgrab6mXTQMThGAdAkfIIG
ocNT1cDXUZzAdqkOVZpyEiuI1cTVkXBmJj75DdpMZW4wHkl19APu/jwt60sw9f08 rWLZZd7TTBpCfIZS+ZYxvX0+unUej1ZmaRwM3uo7GusDj+hC0Um9gF/d2dgRF6PI
xFR/TSuDnG5jObpzew== Hy68pcpWun7JH4rZpQ==
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,7 +0,0 @@
#!/usr/bin/env bash
export RUST_LOG=info,lanspread_client=debug,lanspread_proto=debug
#export RUST_LOG=error
exec cargo run -p lanspread-client -- "$@" < <(while sleep 0.1; do echo "list"; sleep 0.1; echo "get 1"; sleep 0.1; echo "get 25"; done)
#RUST_LOG=info exec cargo run --profile release-lto -p lanspread-client < <(while sleep 0.1; do echo "list"; sleep 0.1; echo "get 1"; sleep 0.1; echo "get 25"; done)

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-client" name = "lanspread-client"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
@ -21,7 +21,7 @@ lanspread-utils = { path = "../lanspread-utils" }
bytes = { workspace = true } bytes = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
eyre = { workspace = true } eyre = { workspace = true }
log = "0.4" log = { workspace = true }
s2n-quic = { workspace = true } s2n-quic = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View File

@ -1,27 +1,171 @@
use std::{net::SocketAddr, time::Duration}; #![allow(clippy::missing_errors_doc)]
use bytes::{Bytes, BytesMut}; use std::{fs::File, io::Write as _, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use lanspread_db::db::Game;
use bytes::BytesMut;
use lanspread_db::db::{Game, GameFileDescription};
use lanspread_proto::{Message as _, Request, Response}; use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr; use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient}; use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::{
io::AsyncWriteExt,
sync::{
RwLock,
mpsc::{UnboundedReceiver, UnboundedSender},
},
};
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem")); static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
#[derive(Debug)] #[derive(Debug)]
pub enum ClientEvent { pub enum ClientEvent {
ListGames(Vec<Game>), ListGames(Vec<Game>),
GotGameFiles {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
DownloadGameFilesBegin {
id: String,
},
DownloadGameFilesFinished {
id: String,
},
DownloadGameFilesFailed {
id: String,
},
} }
#[derive(Debug)] #[derive(Debug)]
pub enum ClientCommand { pub enum ClientCommand {
ListGames, ListGames,
GetGame(String), GetGame(String),
DownloadGameFiles {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
ServerAddr(SocketAddr), ServerAddr(SocketAddr),
SetGameDir(String),
} }
/// # 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
}
async fn receive_game_file(
conn: &mut Connection,
desc: &GameFileDescription,
games_folder: &str,
) -> eyre::Result<()> {
log::info!("downloading: {desc:?}");
let stream = conn.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
let request = Request::GetGameFileData(desc.clone());
// request file
tx.write_all(&request.encode()).await?;
// create file
let path = PathBuf::from(&games_folder).join(&desc.relative_path);
let mut file = File::create(&path)?;
// receive file contents
while let Some(data) = rx.receive().await? {
file.write_all(&data)?;
}
log::debug!("file download complete: {}", path.display());
tx.close().await?;
Ok(())
}
async fn download_game_files(
game_id: &str,
game_file_descs: Vec<GameFileDescription>,
games_folder: String,
server_addr: SocketAddr,
tx_notify_ui: UnboundedSender<ClientEvent>,
) -> eyre::Result<()> {
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
let client = QuicClient::builder()
.with_tls(CERT_PEM)?
.with_io("0.0.0.0:0")?
.with_limits(limits)?
.start()?;
let conn = Connect::new(server_addr).with_server_name("localhost");
let mut conn = client.connect(conn).await?;
conn.keep_alive(true)?;
let game_files = game_file_descs
.iter()
.filter(|desc| !desc.is_dir)
.filter(|desc| !desc.is_version_ini())
.collect::<Vec<_>>();
if game_files.is_empty() {
eyre::bail!("game_file_descs empty: no game files to download");
}
tx_notify_ui.send(ClientEvent::DownloadGameFilesBegin {
id: game_id.to_string(),
})?;
// receive all game files
for file_desc in game_files {
receive_game_file(&mut conn, file_desc, &games_folder).await?;
}
let version_file_desc = game_file_descs
.iter()
.find(|desc| desc.is_version_ini())
.ok_or_else(|| eyre::eyre!("version.ini not found"))?;
// receive version.ini
receive_game_file(&mut conn, version_file_desc, &games_folder).await?;
log::info!("all files downloaded for game: {game_id}");
tx_notify_ui.send(ClientEvent::DownloadGameFilesFinished {
id: game_id.to_string(),
})?;
Ok(())
}
struct Ctx {
game_dir: Arc<RwLock<Option<String>>>,
}
#[allow(clippy::too_many_lines)]
pub async fn run( pub async fn run(
mut rx_control: UnboundedReceiver<ClientCommand>, mut rx_control: UnboundedReceiver<ClientCommand>,
tx_notify_ui: UnboundedSender<ClientEvent>, tx_notify_ui: UnboundedSender<ClientEvent>,
@ -35,10 +179,15 @@ pub async fn run(
} }
}; };
// client context
let ctx = Ctx {
game_dir: Arc::new(RwLock::new(None)),
};
loop { loop {
let limits = Limits::default() let limits = Limits::default()
.with_max_handshake_duration(Duration::from_secs(3))? .with_max_handshake_duration(Duration::from_secs(3))?
.with_max_idle_timeout(Duration::from_secs(1))?; .with_max_idle_timeout(Duration::from_secs(3))?;
let client = QuicClient::builder() let client = QuicClient::builder()
.with_tls(CERT_PEM)? .with_tls(CERT_PEM)?
@ -46,8 +195,8 @@ pub async fn run(
.with_limits(limits)? .with_limits(limits)?
.start()?; .start()?;
let conn = Connect::new(server_addr).with_server_name("localhost"); let connection = Connect::new(server_addr).with_server_name("localhost");
let mut conn = match client.connect(conn).await { let mut conn = match client.connect(connection.clone()).await {
Ok(conn) => conn, Ok(conn) => conn,
Err(e) => { Err(e) => {
log::error!("failed to connect to server: {e}"); log::error!("failed to connect to server: {e}");
@ -58,6 +207,10 @@ pub async fn run(
conn.keep_alive(true)?; conn.keep_alive(true)?;
if !initial_server_alive_check(&mut conn).await {
continue;
}
log::info!( log::info!(
"connected: (server: {}) (client: {})", "connected: (server: {}) (client: {})",
maybe_addr!(conn.remote_addr()), maybe_addr!(conn.remote_addr()),
@ -68,15 +221,85 @@ pub async fn run(
while let Some(cmd) = rx_control.recv().await { while let Some(cmd) = rx_control.recv().await {
let request = match cmd { let request = match cmd {
ClientCommand::ListGames => Request::ListGames, ClientCommand::ListGames => Request::ListGames,
ClientCommand::GetGame(id) => Request::GetGame { id }, ClientCommand::GetGame(id) => {
ClientCommand::ServerAddr(_) => Request::Invalid( log::info!("requesting game from server: {id}");
Bytes::new(), Request::GetGame { id }
"invalid control message (ServerAddr), should not happen".into(), }
), ClientCommand::ServerAddr(_) => {
log::warn!("unexpected ServerAddr command from UI client");
continue;
}
ClientCommand::SetGameDir(game_dir) => {
*ctx.game_dir.write().await = Some(game_dir.clone());
continue;
}
ClientCommand::DownloadGameFiles {
id,
file_descriptions,
} => {
log::info!("got ClientCommand::DownloadGameFiles");
let games_folder = { ctx.game_dir.read().await.clone() };
if let Some(games_folder) = games_folder {
let tx_notify_ui = tx_notify_ui.clone();
tokio::task::spawn(async move {
if let Err(e) = download_game_files(
&id,
file_descriptions,
games_folder,
server_addr,
tx_notify_ui.clone(),
)
.await
{
log::error!("failed to download game files: {e}");
if let Err(e) =
tx_notify_ui.send(ClientEvent::DownloadGameFilesFailed { id })
{
log::error!(
"failed to send DownloadGameFilesFailed event: {e}"
);
}
}
});
} else {
log::error!(
"Cannot handle game file descriptions: games_folder is not set"
);
}
continue;
}
}; };
// we got a command from the UI client
// but it is possible that we lost the connection to the server
// so we check and reconnect if needed
let mut retries = 0;
loop {
if initial_server_alive_check(&mut conn).await {
log::info!("server is back alive! 😊");
break;
}
if retries == 0 {
log::warn!("server connection lost, reconnecting...");
}
retries += 1;
conn = match client.connect(connection.clone()).await {
Ok(conn) => conn,
Err(e) => {
log::warn!("failed to connect to server: {e}");
log::warn!("retrying in 3 seconds...");
tokio::time::sleep(Duration::from_secs(3)).await;
continue;
}
};
}
let data = request.encode(); let data = request.encode();
log::debug!("encoded data: {}", String::from_utf8_lossy(&data)); log::trace!("encoded data: {}", String::from_utf8_lossy(&data));
let stream = match conn.open_bidirectional_stream().await { let stream = match conn.open_bidirectional_stream().await {
Ok(stream) => stream, Ok(stream) => stream,
@ -89,7 +312,7 @@ pub async fn run(
let (mut rx, mut tx) = stream.split(); let (mut rx, mut tx) = stream.split();
if let Err(e) = tx.send(data).await { if let Err(e) = tx.send(data).await {
log::error!("failed to send request to server {:?}", e); log::error!("failed to send request to server {e:?}");
} }
let mut data = BytesMut::new(); let mut data = BytesMut::new();
@ -97,24 +320,61 @@ pub async fn run(
data.extend_from_slice(&bytes); data.extend_from_slice(&bytes);
} }
log::debug!("{} bytes received from server", data.len()); log::debug!("{} bytes received from server", data.len());
log::trace!("server response (RAW): {}", String::from_utf8_lossy(&data)); log::trace!("msg: (raw): {}", String::from_utf8_lossy(&data));
let response = Response::decode(data.freeze()); let response = Response::decode(data.freeze());
log::trace!("server response (DECODED): {response:?}"); log::trace!("msg: {response:?}");
match response { match response {
Response::Games(games) => { Response::ListGames(games) => {
for game in &games { for game in &games {
log::trace!("{game}"); log::trace!("{game}");
} }
if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) { if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) {
log::debug!("failed to send ClientEvent::ListGames to client {e:?}"); log::error!("failed to send ClientEvent::ListGames to client {e:?}");
} else { }
log::info!("sent ClientEvent::ListGames to Tauri client"); }
Response::GetGame {
id,
file_descriptions,
} => {
log::info!(
"got {} game file descriptions from server",
file_descriptions.len()
);
let games_folder = { ctx.game_dir.read().await.clone() };
match games_folder {
Some(games_folder) => {
// create all directories before receiving the actual files
file_descriptions
.iter()
.filter(|f| f.is_dir)
.for_each(|dir| {
let path =
PathBuf::from(&games_folder).join(&dir.relative_path);
if let Err(e) = std::fs::create_dir_all(path) {
log::error!("failed to create directory: {e}");
}
});
if let Err(e) = tx_notify_ui.send(ClientEvent::GotGameFiles {
id,
file_descriptions,
}) {
log::error!(
"failed to send ClientEvent::GotGameFiles to client: {e}"
);
}
}
None => {
log::error!(
"Cannot handle game file descriptions: game_dir is not set"
);
}
} }
} }
Response::Game(game) => log::debug!("game received: {game:?}"),
Response::GameNotFound(id) => log::debug!("game not found {id}"), Response::GameNotFound(id) => log::debug!("game not found {id}"),
Response::InvalidRequest(request_bytes, err) => log::error!( Response::InvalidRequest(request_bytes, err) => log::error!(
"server says our request was invalid (error: {}): {}", "server says our request was invalid (error: {}): {}",
@ -131,6 +391,7 @@ pub async fn run(
String::from_utf8_lossy(&data) String::from_utf8_lossy(&data)
); );
} }
Response::Pong => (), // ignore (should never happen)
} }
if let Err(err) = tx.close().await { if let Err(err) = tx.close().await {

View File

@ -1,10 +1,13 @@
[package] [package]
name = "lanspread-compat" name = "lanspread-compat"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
# local
lanspread-db = { path = "../lanspread-db" }
eyre = { workspace = true } eyre = { workspace = true }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] } sqlx = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View File

@ -1,5 +1,6 @@
use std::path::Path; use std::path::Path;
use lanspread_db::db::Game;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
@ -45,3 +46,21 @@ pub async fn get_games(db: &Path) -> eyre::Result<Vec<EtiGame>> {
Ok(games) Ok(games)
} }
impl From<EtiGame> for Game {
fn from(eti_game: EtiGame) -> Self {
Self {
id: eti_game.game_id,
name: eti_game.game_title,
description: eti_game.game_readme_de,
release_year: eti_game.game_release,
publisher: eti_game.game_publisher,
max_players: eti_game.game_maxplayers,
version: eti_game.game_version,
genre: eti_game.genre_de,
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
thumbnail: None,
installed: false,
}
}
}

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-db" name = "lanspread-db"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -1,7 +1,7 @@
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
#![allow(clippy::doc_markdown)] #![allow(clippy::doc_markdown)]
use std::{collections::HashMap, fmt}; use std::{collections::HashMap, fmt, path::Path};
use bytes::Bytes; use bytes::Bytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -29,6 +29,8 @@ pub struct Game {
pub size: u64, pub size: u64,
/// thumbnail image /// thumbnail image
pub thumbnail: Option<Bytes>, pub thumbnail: Option<Bytes>,
/// only relevant for client (yeah... I know)
pub installed: bool,
} }
impl fmt::Debug for Game { impl fmt::Debug for Game {
@ -76,7 +78,7 @@ pub struct GameDB {
impl GameDB { impl GameDB {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn empty() -> Self {
GameDB { GameDB {
games: HashMap::new(), games: HashMap::new(),
} }
@ -84,13 +86,24 @@ impl GameDB {
#[must_use] #[must_use]
pub fn from(games: Vec<Game>) -> Self { pub fn from(games: Vec<Game>) -> Self {
let mut db = GameDB::new(); let mut db = GameDB::empty();
for game in games { for game in games {
db.games.insert(game.id.clone(), game); db.games.insert(game.id.clone(), game);
} }
db db
} }
pub fn add_thumbnails(&mut self, thumbs_dir: &Path) {
for game in self.games.values_mut() {
let asset = thumbs_dir.join(format!("{}.jpg", game.id));
if let Ok(data) = std::fs::read(&asset) {
game.thumbnail = Some(Bytes::from(data));
} else {
tracing::warn!("Thumbnail missing: {}", game.id);
}
}
}
#[must_use] #[must_use]
pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game> pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
where where
@ -99,6 +112,14 @@ impl GameDB {
self.games.get(id.as_ref()) 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] #[must_use]
pub fn get_game_by_name(&self, name: &str) -> Option<&Game> { pub fn get_game_by_name(&self, name: &str) -> Option<&Game> {
self.games.values().find(|game| game.name == name) self.games.values().find(|game| game.name == name)
@ -110,10 +131,43 @@ impl GameDB {
games.sort_by(|a, b| a.name.cmp(&b.name)); games.sort_by(|a, b| a.name.cmp(&b.name));
games games
} }
pub fn set_all_uninstalled(&mut self) {
for game in self.games.values_mut() {
game.installed = false;
}
}
} }
impl Default for GameDB { impl Default for GameDB {
fn default() -> Self { fn default() -> Self {
Self::new() Self::empty()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct GameFileDescription {
pub game_id: String,
pub relative_path: String,
pub is_dir: bool,
}
impl GameFileDescription {
#[must_use]
pub fn is_version_ini(&self) -> bool {
self.relative_path.ends_with("/version.ini")
}
}
impl fmt::Debug for GameFileDescription {
#[allow(clippy::cast_precision_loss)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: [{}] path:{}",
self.game_id,
if self.is_dir { 'D' } else { 'F' },
self.relative_path,
)
} }
} }

View File

@ -1,9 +1,9 @@
[package] [package]
name = "lanspread-mdns" name = "lanspread-mdns"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
mdns-sd = "0.11" mdns-sd = { workspace = true }
eyre = { workspace = true } eyre = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-proto" name = "lanspread-proto"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
@ -16,7 +16,7 @@ unwrap_used = "warn"
lanspread-db = { path = "../lanspread-db" } lanspread-db = { path = "../lanspread-db" }
# external # external
bytes = "1.8" bytes = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View File

@ -1,19 +1,24 @@
use bytes::Bytes; use bytes::Bytes;
use lanspread_db::db::Game; use lanspread_db::db::{Game, GameFileDescription};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::error;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Request { pub enum Request {
Ping,
ListGames, ListGames,
GetGame { id: String }, GetGame { id: String },
GetGameFileData(GameFileDescription),
Invalid(Bytes, String), Invalid(Bytes, String),
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Response { pub enum Response {
Games(Vec<Game>), Pong,
Game(Game), ListGames(Vec<Game>),
GetGame {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
GameNotFound(String), GameNotFound(String),
InvalidRequest(Bytes, String), InvalidRequest(Bytes, String),
EncodingError(String), EncodingError(String),
@ -32,11 +37,7 @@ impl Message for Request {
match serde_json::from_slice(&bytes) { match serde_json::from_slice(&bytes) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
tracing::error!( tracing::error!(?e, "Request decoding error");
"got invalid request from client (error: {}): {}",
e,
String::from_utf8_lossy(&bytes)
);
Request::Invalid(bytes, e.to_string()) Request::Invalid(bytes, e.to_string())
} }
} }
@ -46,7 +47,7 @@ impl Message for Request {
match serde_json::to_vec(self) { match serde_json::to_vec(self) {
Ok(s) => Bytes::from(s), Ok(s) => Bytes::from(s),
Err(e) => { Err(e) => {
error!(?e, "Request encoding error"); tracing::error!(?e, "Request encoding error");
Bytes::from(format!(r#"{{"error": "encoding error: {e}"}}"#)) Bytes::from(format!(r#"{{"error": "encoding error: {e}"}}"#))
} }
} }
@ -58,7 +59,10 @@ impl Message for Response {
fn decode(bytes: Bytes) -> Self { fn decode(bytes: Bytes) -> Self {
match serde_json::from_slice(&bytes) { match serde_json::from_slice(&bytes) {
Ok(t) => t, Ok(t) => t,
Err(e) => Response::DecodingError(bytes, e.to_string()), Err(e) => {
tracing::error!(?e, "Response decoding error");
Response::DecodingError(bytes, e.to_string())
}
} }
} }
@ -66,22 +70,9 @@ impl Message for Response {
match serde_json::to_vec(self) { match serde_json::to_vec(self) {
Ok(s) => Bytes::from(s), Ok(s) => Bytes::from(s),
Err(e) => { Err(e) => {
error!(?e, "Response encoding error"); tracing::error!(?e, "Response encoding error");
Bytes::from(format!(r#"{{"error": "encoding error: {e}"}}"#)) Bytes::from(format!(r#"{{"error": "encoding error: {e}"}}"#))
} }
} }
} }
} }
// Helper methods for Response
impl Response {
#[must_use]
pub fn games(games: Vec<Game>) -> Self {
Response::Games(games)
}
#[must_use]
pub fn game(game: Game) -> Self {
Response::Game(game)
}
}

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-server" name = "lanspread-server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
@ -21,8 +21,10 @@ lanspread-utils = { path = "../lanspread-utils" }
# external # external
bytes = { workspace = true } bytes = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
eyre = { workspace = true } eyre = { workspace = true }
gethostname = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
s2n-quic = { workspace = true } s2n-quic = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
@ -30,3 +32,5 @@ semver = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
uuid = { workspace = true }
walkdir = { workspace = true }

View File

@ -1,24 +0,0 @@
use std::path::PathBuf;
use bytes::Bytes;
pub(crate) struct Thumbnails {
folder: PathBuf,
}
impl Thumbnails {
pub(crate) fn new(folder: PathBuf) -> Thumbnails {
Thumbnails { folder }
}
pub(crate) fn get(&self, path: &str) -> Option<Bytes> {
let asset = self.folder.join(format!("{path}.jpg"));
if let Ok(data) = std::fs::read(asset) {
return Some(Bytes::from(data));
}
tracing::warn!("Thumbnail not found: {path}");
None
}
}

View File

@ -0,0 +1,23 @@
use std::{net::IpAddr, path::PathBuf};
use clap::Parser;
#[allow(clippy::doc_markdown)]
#[derive(Debug, Parser)]
pub(crate) struct Cli {
/// IP address to bind to.
#[clap(long)]
pub(crate) ip: IpAddr,
/// Listen port.
#[clap(long)]
pub(crate) port: u16,
/// Game database path (SQLite).
#[clap(long)]
pub(crate) db: PathBuf,
/// Games folder.
#[clap(long)]
pub(crate) game_dir: PathBuf,
/// Thumbnails folder.
#[clap(long)]
pub(crate) thumbs_dir: PathBuf,
}

View File

@ -1,191 +1,87 @@
#![allow(clippy::doc_markdown)] mod cli;
mod quic;
mod req;
use std::{ use std::{convert::Into, net::SocketAddr, time::Duration};
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::Arc,
};
use assets::Thumbnails; use chrono::{DateTime, Local};
use clap::Parser; use clap::Parser as _;
use lanspread_compat::eti::{self, EtiGame}; use cli::Cli;
use gethostname::gethostname;
use lanspread_compat::eti;
use lanspread_db::db::{Game, GameDB}; use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{ use lanspread_mdns::{
DaemonEvent, DaemonEvent, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser,
MdnsAdvertiser,
LANSPREAD_INSTANCE_NAME,
LANSPREAD_SERVICE_TYPE,
}; };
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::Server as QuicServer;
use tokio::{io::AsyncWriteExt, sync::Mutex};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use uuid::Uuid;
mod assets; fn spawn_mdns_task(server_addr: SocketAddr) -> eyre::Result<()> {
let combined_str = if 1 == 2 {
let peer_id = Uuid::now_v7().simple().to_string();
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem")); let uidddd = Uuid::now_v7();
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
struct Server; // TODO
let uidddd = uidddd
.get_timestamp()
.expect("failed to get timestamp from UUID")
.to_unix();
#[derive(Clone, Debug)] let local_datetime: DateTime<Local> =
struct ServerCtx { DateTime::from_timestamp(i64::try_from(uidddd.0).unwrap_or(0), uidddd.1)
handler: RequestHandler, .expect("Failed to create DateTime from uuid unix timestamp")
} .into();
#[derive(Clone, Debug)] dbg!(local_datetime);
struct ConnectionCtx {
server_ctx: Arc<ServerCtx>,
remote_addr: String,
}
impl Server { let hostname = gethostname();
async fn run(addr: SocketAddr, db: GameDB) -> eyre::Result<()> { let mut hostname = hostname.to_str().unwrap_or("");
let mut server = QuicServer::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io(addr)?
.start()?;
let server_ctx = Arc::new(ServerCtx { if hostname.len() + peer_id.len() > 63 {
handler: RequestHandler::new(db), hostname = &hostname[..63 - peer_id.len()];
}); }
format!("{hostname}-{peer_id}")
} else {
String::from(LANSPREAD_INSTANCE_NAME)
};
let mdns = MdnsAdvertiser::new(LANSPREAD_SERVICE_TYPE, &combined_str, server_addr)?;
while let Some(mut connection) = server.accept().await {
let conn_ctx = Arc::new(ConnectionCtx {
server_ctx: server_ctx.clone(),
remote_addr: maybe_addr!(connection.remote_addr()),
});
// spawn a new task for the connection
tokio::spawn(async move { tokio::spawn(async move {
tracing::info!("{} connected", conn_ctx.remote_addr); while let Ok(event) = mdns.monitor.recv() {
tracing::trace!("mDNS: {:?}", &event);
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await { if let DaemonEvent::Error(e) = event {
let (mut rx, mut tx) = stream.split(); tracing::error!("mDNS: {e}");
tokio::time::sleep(Duration::from_secs(1)).await;
let conn_ctx = conn_ctx.clone();
// spawn a new task for the stream
tokio::spawn(async move {
tracing::trace!("{} stream opened", conn_ctx.remote_addr);
// handle streams
while let Ok(Some(data)) = rx.receive().await {
tracing::trace!(
"{} client request (raw): {}",
conn_ctx.remote_addr,
String::from_utf8_lossy(&data)
);
let request = Request::decode(data);
tracing::debug!(
"{} client request (decoded): {:?}",
conn_ctx.remote_addr,
request
);
let response =
conn_ctx.server_ctx.handler.handle_request(request).await;
tracing::trace!(
"{} server response: {:?}",
conn_ctx.remote_addr,
response
);
let raw_response = response.encode();
tracing::trace!(
"{} server response (raw): {}",
conn_ctx.remote_addr,
String::from_utf8_lossy(&raw_response)
);
// write response back to client
if let Err(e) = tx.write_all(&raw_response).await {
tracing::error!(?e);
}
// close the stream
if let Err(e) = tx.close().await {
tracing::error!(?e);
} }
} }
}); });
}
});
}
Ok(()) Ok(())
}
} }
#[derive(Clone, Debug)] async fn prepare_game_db(cli: &Cli) -> eyre::Result<GameDB> {
struct RequestHandler { // build games from ETI database
db: Arc<Mutex<GameDB>>, let mut games: Vec<Game> = eti::get_games(&cli.db)
} .await?
.into_iter()
.map(Into::into)
.collect();
impl RequestHandler { // filter out games that the server does not have in game_dir
fn new(games: GameDB) -> RequestHandler { games.retain(|game| cli.game_dir.join(&game.id).is_dir());
RequestHandler {
db: Arc::new(Mutex::new(games)),
}
}
async fn handle_request(&self, request: Request) -> Response { let mut game_db = GameDB::from(games);
match request {
Request::ListGames => {
let db = self.db.lock().await;
Response::Games(db.all_games().into_iter().cloned().collect())
}
Request::GetGame { id } => {
let db = self.db.lock().await;
match db.get_game_by_id(&id) {
Some(game) => Response::Game(game.clone()),
None => Response::GameNotFound(id),
}
}
Request::Invalid(data, err_msg) => {
tracing::error!(
"got invalid request from client (error: {}): {}",
err_msg,
String::from_utf8_lossy(&data)
);
Response::InvalidRequest(data, err_msg)
}
}
}
}
#[derive(Debug, Parser)] game_db.add_thumbnails(&cli.thumbs_dir);
struct Cli {
/// IP address to bind to.
#[clap(long)]
ip: IpAddr,
/// Listen port.
#[clap(long)]
port: u16,
/// Game database path (SQLite).
#[clap(long)]
db: PathBuf,
/// Games folder.
#[clap(long)]
folder: PathBuf,
/// Thumbnails folder.
#[clap(long)]
thumbnails: PathBuf,
}
fn eti_game_to_game(eti_game: EtiGame) -> Game { game_db.all_games().iter().for_each(|game| {
#[allow(clippy::cast_possible_truncation)] tracing::debug!("Found game: {game}");
#[allow(clippy::cast_sign_loss)] });
Game { tracing::info!("Prepared game database with {} games", game_db.games.len());
id: eti_game.game_id,
name: eti_game.game_title, Ok(game_db)
description: eti_game.game_readme_de,
release_year: eti_game.game_release,
publisher: eti_game.game_publisher,
max_players: eti_game.game_maxplayers,
version: eti_game.game_version,
genre: eti_game.genre_de,
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
thumbnail: None,
}
} }
#[tokio::main] #[tokio::main]
@ -196,38 +92,19 @@ async fn main() -> eyre::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
let eti_games = eti::get_games(&cli.db).await?; assert!(
let mut games: Vec<Game> = eti_games.into_iter().map(eti_game_to_game).collect(); cli.game_dir.exists(),
let thumbnails = Thumbnails::new(cli.thumbnails); "Games folder does not exist: {}",
cli.game_dir.to_str().expect("Invalid path")
);
// add thumbnails to games let server_addr = SocketAddr::from((cli.ip, cli.port));
for game in &mut games {
if let Some(thumbnail) = thumbnails.get(&game.id) {
game.thumbnail = Some(thumbnail);
} else {
tracing::warn!("No thumbnail found: {}", game.id);
}
}
let game_db = GameDB::from(games); // spawn mDNS listener task
spawn_mdns_task(server_addr)?;
let mdns = MdnsAdvertiser::new( let game_db = prepare_game_db(&cli).await?;
LANSPREAD_SERVICE_TYPE,
LANSPREAD_INSTANCE_NAME,
(cli.ip, cli.port).into(),
)?;
tokio::spawn(async move { tracing::info!("Server listening on {server_addr}");
while let Ok(event) = mdns.monitor.recv() { crate::quic::run_server(server_addr, game_db, cli.game_dir).await
tracing::info!("mDNS: {:?}", &event);
if let DaemonEvent::Error(e) = event {
tracing::error!("mDNS: {e}");
break;
}
}
});
tracing::info!("Server listening on {}:{}", cli.ip, cli.port);
Server::run(SocketAddr::from((cli.ip, cli.port)), game_db).await
} }

View File

@ -0,0 +1,119 @@
use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use lanspread_db::db::GameDB;
use lanspread_proto::{Message as _, Request};
use lanspread_utils::maybe_addr;
use s2n_quic::{Connection, Server, provider::limits::Limits, stream::BidirectionalStream};
use crate::req::{RequestHandler, send_game_file_data};
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem"));
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
#[derive(Clone, Debug)]
struct ServerCtx {
handler: RequestHandler,
games_folder: PathBuf,
}
async fn handle_bidi_stream(stream: BidirectionalStream, ctx: Arc<ServerCtx>) -> eyre::Result<()> {
let (mut rx, mut tx) = stream.split();
let remote_addr = maybe_addr!(rx.connection().remote_addr());
tracing::trace!("{remote_addr} stream opened");
// handle streams
loop {
match rx.receive().await {
Ok(Some(data)) => {
tracing::trace!(
"{remote_addr} msg: (raw): {}",
String::from_utf8_lossy(&data)
);
let request = Request::decode(data);
tracing::debug!("{remote_addr} msg: {request:?}");
// special case for now (send game file data to client)
if let Request::GetGameFileData(game_file_desc) = &request {
send_game_file_data(game_file_desc, &mut tx, &ctx.games_folder).await;
continue;
}
// normal case (handle request)
if let Err(e) = ctx
.handler
.handle_request(request, &ctx.games_folder, &mut tx)
.await
{
tracing::error!(?e, "{remote_addr} error handling request");
}
}
Ok(None) => {
tracing::trace!("{remote_addr} stream closed");
break;
}
Err(e) => {
tracing::error!("{remote_addr} stream error: {e}");
break;
}
}
}
Ok(())
}
async fn handle_connection(mut connection: Connection, ctx: Arc<ServerCtx>) -> eyre::Result<()> {
let remote_addr = maybe_addr!(connection.remote_addr());
tracing::info!("{remote_addr} connected");
// handle streams
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
let ctx = ctx.clone();
let remote_addr = remote_addr.clone();
// spawn a new task for the stream
tokio::spawn(async move {
if let Err(e) = handle_bidi_stream(stream, ctx).await {
tracing::error!("{remote_addr} stream error: {e}");
}
});
}
Ok(())
}
pub(crate) async fn run_server(
addr: SocketAddr,
db: GameDB,
games_folder: PathBuf,
) -> eyre::Result<()> {
let limits = Limits::default()
.with_max_handshake_duration(Duration::from_secs(3))?
.with_max_idle_timeout(Duration::from_secs(3))?;
let mut server = Server::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io(addr)?
.with_limits(limits)?
.start()?;
let ctx = Arc::new(ServerCtx {
handler: RequestHandler::new(db),
games_folder,
});
while let Some(connection) = server.accept().await {
let ctx = ctx.clone();
// spawn a new task for the connection
tokio::spawn(async move {
if let Err(e) = handle_connection(connection, ctx).await {
tracing::error!("Connection error: {}", e);
}
});
}
tracing::info!("Server shutting down");
Ok(())
}

View File

@ -0,0 +1,175 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use bytes::{Bytes, BytesMut};
use lanspread_db::db::{GameDB, GameFileDescription};
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::stream::SendStream;
use tokio::{io::AsyncReadExt, sync::RwLock, time::Instant};
use walkdir::WalkDir;
#[derive(Clone, Debug)]
pub(crate) struct RequestHandler {
db: Arc<RwLock<GameDB>>,
}
impl RequestHandler {
pub(crate) fn new(games: GameDB) -> RequestHandler {
RequestHandler {
db: Arc::new(RwLock::new(games)),
}
}
pub(crate) async fn handle_request(
&self,
request: Request,
games_folder: &Path,
tx: &mut SendStream,
) -> eyre::Result<()> {
let remote_addr = maybe_addr!(tx.connection().remote_addr());
// process request and generate response
let response = self.process_request(request, games_folder).await;
tracing::trace!("{remote_addr} server response: {response:?}");
// write response back to client
tx.send(response.encode()).await?;
// close the stream
tx.close().await?;
Ok(())
}
pub(crate) async fn process_request(&self, request: Request, games_folder: &Path) -> Response {
match request {
Request::Ping => Response::Pong,
Request::ListGames => {
let db = self.db.read().await;
Response::ListGames(db.all_games().into_iter().cloned().collect())
}
Request::GetGame { id } => {
if self.db.read().await.get_game_by_id(&id).is_none() {
tracing::error!("Game not found in DB: {id}");
return Response::GameNotFound(id);
}
let game_dir = games_folder.join(&id);
if !game_dir.exists() {
tracing::error!("Game folder does not exist: {}", game_dir.display());
return Response::GameNotFound(id);
}
let mut game_files_descs: Vec<GameFileDescription> = vec![];
for entry in WalkDir::new(&game_dir)
.into_iter()
.filter_map(std::result::Result::ok)
{
match get_relative_path(games_folder, entry.path()) {
Ok(relative_path) => match relative_path.to_str() {
Some(relative_path) => {
let game_file_description = GameFileDescription {
game_id: id.clone(),
relative_path: relative_path.to_string(),
is_dir: entry.file_type().is_dir(),
};
tracing::debug!("Found game file: {:?}", game_file_description);
game_files_descs.push(game_file_description);
}
None => {
tracing::error!("Failed to get relative path: {relative_path:?}",);
}
},
Err(e) => {
tracing::error!("Failed to get relative path: {e}");
}
}
}
Response::GetGame {
id,
file_descriptions: game_files_descs,
}
}
Request::GetGameFileData(_) => {
Response::InvalidRequest(Bytes::new(), "Not implemented".to_string())
}
Request::Invalid(data, err_msg) => Response::InvalidRequest(data, err_msg),
}
}
}
pub(crate) async fn send_game_file_data(
game_file_desc: &GameFileDescription,
tx: &mut SendStream,
game_dir: &Path,
) {
let remote_addr = maybe_addr!(tx.connection().remote_addr());
tracing::debug!("{remote_addr} client requested game file data: {game_file_desc:?}",);
// deliver file data to client
let game_file = game_dir.join(&game_file_desc.relative_path);
let mut total_bytes = 0;
let mut last_total_bytes = 0;
let mut timestamp = Instant::now();
if let Ok(mut f) = tokio::fs::File::open(&game_file).await {
let mut buf = BytesMut::with_capacity(64 * 1024);
while let Ok(bytes_read) = f.read_buf(&mut buf).await {
if bytes_read == 0 {
break;
}
total_bytes += bytes_read;
if last_total_bytes + 10_000_000 < total_bytes {
let elapsed = timestamp.elapsed();
let diff_bytes = total_bytes - last_total_bytes;
if elapsed.as_secs_f64() >= 1.0 {
#[allow(clippy::cast_precision_loss)]
let mb_per_s = (diff_bytes as f64) / (elapsed.as_secs_f64() * 1000.0 * 1000.0);
tracing::debug!(
"{remote_addr} sending file data: {game_file:?}, MB/s: {mb_per_s:.2}",
);
last_total_bytes = total_bytes;
timestamp = Instant::now();
}
}
if let Err(e) = tx.send(buf.split_to(bytes_read).freeze()).await {
tracing::error!("{remote_addr} failed to send file data: {e}",);
break;
}
}
tracing::debug!(
"{remote_addr} finished sending file data: {game_file:?}, total_bytes: {total_bytes}",
);
} else {
tracing::error!("{remote_addr} failed to open file: {}", game_file.display());
}
if let Err(e) = tx.close().await {
tracing::error!("{remote_addr} failed to close stream: {e}");
}
}
fn get_relative_path(base: &Path, deep_path: &Path) -> std::io::Result<PathBuf> {
let base_canonical = base.canonicalize()?;
let full_canonical = deep_path.canonicalize()?;
full_canonical
.strip_prefix(&base_canonical)
.map(std::path::Path::to_path_buf)
.map_err(|_| std::io::Error::other("Path is not within base directory"))
}

View File

@ -1,16 +1,19 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"npm:@tauri-apps/api@2": "2.1.1", "npm:@tauri-apps/api@^2.3.0": "2.3.0",
"npm:@tauri-apps/cli@2": "2.1.0", "npm:@tauri-apps/cli@^2.3.1": "2.3.1",
"npm:@tauri-apps/plugin-shell@2": "2.0.1", "npm:@tauri-apps/plugin-dialog@^2.2.0": "2.2.0",
"npm:@types/react-dom@^18.2.7": "18.3.1", "npm:@tauri-apps/plugin-shell@^2.2.0": "2.2.0",
"npm:@types/react@^18.2.15": "18.3.12", "npm:@tauri-apps/plugin-store@^2.2.0": "2.2.0",
"npm:@vitejs/plugin-react@^4.2.1": "4.3.3_vite@5.4.11_@babel+core@7.26.0", "npm:@types/react-dom@^19.0.4": "19.0.4_@types+react@19.0.10",
"npm:react-dom@^18.2.0": "18.3.1_react@18.3.1", "npm:@types/react@^19.0.10": "19.0.10",
"npm:react@^18.2.0": "18.3.1", "npm:@types/react@^19.0.12": "19.0.12",
"npm:typescript@^5.2.2": "5.6.3", "npm:@vitejs/plugin-react@^4.3.4": "4.3.4_vite@6.2.2_@babel+core@7.26.10",
"npm:vite@^5.3.1": "5.4.11" "npm:react-dom@19": "19.0.0_react@19.0.0",
"npm:react@19": "19.0.0",
"npm:typescript@^5.8.2": "5.8.2",
"npm:vite@^6.2.2": "6.2.2"
}, },
"npm": { "npm": {
"@ampproject/remapping@2.3.0": { "@ampproject/remapping@2.3.0": {
@ -28,11 +31,11 @@
"picocolors" "picocolors"
] ]
}, },
"@babel/compat-data@7.26.2": { "@babel/compat-data@7.26.8": {
"integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==" "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="
}, },
"@babel/core@7.26.0": { "@babel/core@7.26.10": {
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dependencies": [ "dependencies": [
"@ampproject/remapping", "@ampproject/remapping",
"@babel/code-frame", "@babel/code-frame",
@ -51,8 +54,8 @@
"semver" "semver"
] ]
}, },
"@babel/generator@7.26.2": { "@babel/generator@7.26.10": {
"integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
"dependencies": [ "dependencies": [
"@babel/parser", "@babel/parser",
"@babel/types", "@babel/types",
@ -61,8 +64,8 @@
"jsesc" "jsesc"
] ]
}, },
"@babel/helper-compilation-targets@7.25.9": { "@babel/helper-compilation-targets@7.26.5": {
"integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
"dependencies": [ "dependencies": [
"@babel/compat-data", "@babel/compat-data",
"@babel/helper-validator-option", "@babel/helper-validator-option",
@ -78,7 +81,7 @@
"@babel/types" "@babel/types"
] ]
}, },
"@babel/helper-module-transforms@7.26.0_@babel+core@7.26.0": { "@babel/helper-module-transforms@7.26.0_@babel+core@7.26.10": {
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
"dependencies": [ "dependencies": [
"@babel/core", "@babel/core",
@ -87,8 +90,8 @@
"@babel/traverse" "@babel/traverse"
] ]
}, },
"@babel/helper-plugin-utils@7.25.9": { "@babel/helper-plugin-utils@7.26.5": {
"integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==" "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="
}, },
"@babel/helper-string-parser@7.25.9": { "@babel/helper-string-parser@7.25.9": {
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="
@ -99,43 +102,43 @@
"@babel/helper-validator-option@7.25.9": { "@babel/helper-validator-option@7.25.9": {
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==" "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="
}, },
"@babel/helpers@7.26.0": { "@babel/helpers@7.26.10": {
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"dependencies": [ "dependencies": [
"@babel/template", "@babel/template",
"@babel/types" "@babel/types"
] ]
}, },
"@babel/parser@7.26.2": { "@babel/parser@7.26.10": {
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"dependencies": [ "dependencies": [
"@babel/types" "@babel/types"
] ]
}, },
"@babel/plugin-transform-react-jsx-self@7.25.9_@babel+core@7.26.0": { "@babel/plugin-transform-react-jsx-self@7.25.9_@babel+core@7.26.10": {
"integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
"dependencies": [ "dependencies": [
"@babel/core", "@babel/core",
"@babel/helper-plugin-utils" "@babel/helper-plugin-utils"
] ]
}, },
"@babel/plugin-transform-react-jsx-source@7.25.9_@babel+core@7.26.0": { "@babel/plugin-transform-react-jsx-source@7.25.9_@babel+core@7.26.10": {
"integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
"dependencies": [ "dependencies": [
"@babel/core", "@babel/core",
"@babel/helper-plugin-utils" "@babel/helper-plugin-utils"
] ]
}, },
"@babel/template@7.25.9": { "@babel/template@7.26.9": {
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"dependencies": [ "dependencies": [
"@babel/code-frame", "@babel/code-frame",
"@babel/parser", "@babel/parser",
"@babel/types" "@babel/types"
] ]
}, },
"@babel/traverse@7.25.9": { "@babel/traverse@7.26.10": {
"integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==",
"dependencies": [ "dependencies": [
"@babel/code-frame", "@babel/code-frame",
"@babel/generator", "@babel/generator",
@ -146,84 +149,90 @@
"globals" "globals"
] ]
}, },
"@babel/types@7.26.0": { "@babel/types@7.26.10": {
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"dependencies": [ "dependencies": [
"@babel/helper-string-parser", "@babel/helper-string-parser",
"@babel/helper-validator-identifier" "@babel/helper-validator-identifier"
] ]
}, },
"@esbuild/aix-ppc64@0.21.5": { "@esbuild/aix-ppc64@0.25.1": {
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==" "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="
}, },
"@esbuild/android-arm64@0.21.5": { "@esbuild/android-arm64@0.25.1": {
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==" "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="
}, },
"@esbuild/android-arm@0.21.5": { "@esbuild/android-arm@0.25.1": {
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==" "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="
}, },
"@esbuild/android-x64@0.21.5": { "@esbuild/android-x64@0.25.1": {
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==" "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="
}, },
"@esbuild/darwin-arm64@0.21.5": { "@esbuild/darwin-arm64@0.25.1": {
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==" "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="
}, },
"@esbuild/darwin-x64@0.21.5": { "@esbuild/darwin-x64@0.25.1": {
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==" "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="
}, },
"@esbuild/freebsd-arm64@0.21.5": { "@esbuild/freebsd-arm64@0.25.1": {
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==" "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="
}, },
"@esbuild/freebsd-x64@0.21.5": { "@esbuild/freebsd-x64@0.25.1": {
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==" "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="
}, },
"@esbuild/linux-arm64@0.21.5": { "@esbuild/linux-arm64@0.25.1": {
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==" "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="
}, },
"@esbuild/linux-arm@0.21.5": { "@esbuild/linux-arm@0.25.1": {
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==" "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="
}, },
"@esbuild/linux-ia32@0.21.5": { "@esbuild/linux-ia32@0.25.1": {
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==" "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="
}, },
"@esbuild/linux-loong64@0.21.5": { "@esbuild/linux-loong64@0.25.1": {
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==" "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="
}, },
"@esbuild/linux-mips64el@0.21.5": { "@esbuild/linux-mips64el@0.25.1": {
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==" "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="
}, },
"@esbuild/linux-ppc64@0.21.5": { "@esbuild/linux-ppc64@0.25.1": {
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==" "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="
}, },
"@esbuild/linux-riscv64@0.21.5": { "@esbuild/linux-riscv64@0.25.1": {
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==" "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="
}, },
"@esbuild/linux-s390x@0.21.5": { "@esbuild/linux-s390x@0.25.1": {
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==" "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="
}, },
"@esbuild/linux-x64@0.21.5": { "@esbuild/linux-x64@0.25.1": {
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==" "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="
}, },
"@esbuild/netbsd-x64@0.21.5": { "@esbuild/netbsd-arm64@0.25.1": {
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==" "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="
}, },
"@esbuild/openbsd-x64@0.21.5": { "@esbuild/netbsd-x64@0.25.1": {
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==" "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="
}, },
"@esbuild/sunos-x64@0.21.5": { "@esbuild/openbsd-arm64@0.25.1": {
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==" "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="
}, },
"@esbuild/win32-arm64@0.21.5": { "@esbuild/openbsd-x64@0.25.1": {
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==" "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="
}, },
"@esbuild/win32-ia32@0.21.5": { "@esbuild/sunos-x64@0.25.1": {
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==" "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="
}, },
"@esbuild/win32-x64@0.21.5": { "@esbuild/win32-arm64@0.25.1": {
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==" "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="
}, },
"@jridgewell/gen-mapping@0.3.5": { "@esbuild/win32-ia32@0.25.1": {
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="
},
"@esbuild/win32-x64@0.25.1": {
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="
},
"@jridgewell/gen-mapping@0.3.8": {
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dependencies": [ "dependencies": [
"@jridgewell/set-array", "@jridgewell/set-array",
"@jridgewell/sourcemap-codec", "@jridgewell/sourcemap-codec",
@ -246,95 +255,98 @@
"@jridgewell/sourcemap-codec" "@jridgewell/sourcemap-codec"
] ]
}, },
"@rollup/rollup-android-arm-eabi@4.26.0": { "@rollup/rollup-android-arm-eabi@4.36.0": {
"integrity": "sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==" "integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w=="
}, },
"@rollup/rollup-android-arm64@4.26.0": { "@rollup/rollup-android-arm64@4.36.0": {
"integrity": "sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==" "integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg=="
}, },
"@rollup/rollup-darwin-arm64@4.26.0": { "@rollup/rollup-darwin-arm64@4.36.0": {
"integrity": "sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==" "integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw=="
}, },
"@rollup/rollup-darwin-x64@4.26.0": { "@rollup/rollup-darwin-x64@4.36.0": {
"integrity": "sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==" "integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA=="
}, },
"@rollup/rollup-freebsd-arm64@4.26.0": { "@rollup/rollup-freebsd-arm64@4.36.0": {
"integrity": "sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==" "integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg=="
}, },
"@rollup/rollup-freebsd-x64@4.26.0": { "@rollup/rollup-freebsd-x64@4.36.0": {
"integrity": "sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==" "integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ=="
}, },
"@rollup/rollup-linux-arm-gnueabihf@4.26.0": { "@rollup/rollup-linux-arm-gnueabihf@4.36.0": {
"integrity": "sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==" "integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg=="
}, },
"@rollup/rollup-linux-arm-musleabihf@4.26.0": { "@rollup/rollup-linux-arm-musleabihf@4.36.0": {
"integrity": "sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==" "integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg=="
}, },
"@rollup/rollup-linux-arm64-gnu@4.26.0": { "@rollup/rollup-linux-arm64-gnu@4.36.0": {
"integrity": "sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==" "integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A=="
}, },
"@rollup/rollup-linux-arm64-musl@4.26.0": { "@rollup/rollup-linux-arm64-musl@4.36.0": {
"integrity": "sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==" "integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw=="
}, },
"@rollup/rollup-linux-powerpc64le-gnu@4.26.0": { "@rollup/rollup-linux-loongarch64-gnu@4.36.0": {
"integrity": "sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==" "integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg=="
}, },
"@rollup/rollup-linux-riscv64-gnu@4.26.0": { "@rollup/rollup-linux-powerpc64le-gnu@4.36.0": {
"integrity": "sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==" "integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg=="
}, },
"@rollup/rollup-linux-s390x-gnu@4.26.0": { "@rollup/rollup-linux-riscv64-gnu@4.36.0": {
"integrity": "sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==" "integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA=="
}, },
"@rollup/rollup-linux-x64-gnu@4.26.0": { "@rollup/rollup-linux-s390x-gnu@4.36.0": {
"integrity": "sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==" "integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag=="
}, },
"@rollup/rollup-linux-x64-musl@4.26.0": { "@rollup/rollup-linux-x64-gnu@4.36.0": {
"integrity": "sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==" "integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ=="
}, },
"@rollup/rollup-win32-arm64-msvc@4.26.0": { "@rollup/rollup-linux-x64-musl@4.36.0": {
"integrity": "sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==" "integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ=="
}, },
"@rollup/rollup-win32-ia32-msvc@4.26.0": { "@rollup/rollup-win32-arm64-msvc@4.36.0": {
"integrity": "sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==" "integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A=="
}, },
"@rollup/rollup-win32-x64-msvc@4.26.0": { "@rollup/rollup-win32-ia32-msvc@4.36.0": {
"integrity": "sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==" "integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ=="
}, },
"@tauri-apps/api@2.1.1": { "@rollup/rollup-win32-x64-msvc@4.36.0": {
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==" "integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="
}, },
"@tauri-apps/cli-darwin-arm64@2.1.0": { "@tauri-apps/api@2.3.0": {
"integrity": "sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==" "integrity": "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="
}, },
"@tauri-apps/cli-darwin-x64@2.1.0": { "@tauri-apps/cli-darwin-arm64@2.3.1": {
"integrity": "sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==" "integrity": "sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw=="
}, },
"@tauri-apps/cli-linux-arm-gnueabihf@2.1.0": { "@tauri-apps/cli-darwin-x64@2.3.1": {
"integrity": "sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==" "integrity": "sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA=="
}, },
"@tauri-apps/cli-linux-arm64-gnu@2.1.0": { "@tauri-apps/cli-linux-arm-gnueabihf@2.3.1": {
"integrity": "sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==" "integrity": "sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA=="
}, },
"@tauri-apps/cli-linux-arm64-musl@2.1.0": { "@tauri-apps/cli-linux-arm64-gnu@2.3.1": {
"integrity": "sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==" "integrity": "sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg=="
}, },
"@tauri-apps/cli-linux-x64-gnu@2.1.0": { "@tauri-apps/cli-linux-arm64-musl@2.3.1": {
"integrity": "sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==" "integrity": "sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w=="
}, },
"@tauri-apps/cli-linux-x64-musl@2.1.0": { "@tauri-apps/cli-linux-x64-gnu@2.3.1": {
"integrity": "sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==" "integrity": "sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA=="
}, },
"@tauri-apps/cli-win32-arm64-msvc@2.1.0": { "@tauri-apps/cli-linux-x64-musl@2.3.1": {
"integrity": "sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==" "integrity": "sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA=="
}, },
"@tauri-apps/cli-win32-ia32-msvc@2.1.0": { "@tauri-apps/cli-win32-arm64-msvc@2.3.1": {
"integrity": "sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==" "integrity": "sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww=="
}, },
"@tauri-apps/cli-win32-x64-msvc@2.1.0": { "@tauri-apps/cli-win32-ia32-msvc@2.3.1": {
"integrity": "sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==" "integrity": "sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw=="
}, },
"@tauri-apps/cli@2.1.0": { "@tauri-apps/cli-win32-x64-msvc@2.3.1": {
"integrity": "sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==", "integrity": "sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q=="
},
"@tauri-apps/cli@2.3.1": {
"integrity": "sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==",
"dependencies": [ "dependencies": [
"@tauri-apps/cli-darwin-arm64", "@tauri-apps/cli-darwin-arm64",
"@tauri-apps/cli-darwin-x64", "@tauri-apps/cli-darwin-x64",
@ -348,8 +360,20 @@
"@tauri-apps/cli-win32-x64-msvc" "@tauri-apps/cli-win32-x64-msvc"
] ]
}, },
"@tauri-apps/plugin-shell@2.0.1": { "@tauri-apps/plugin-dialog@2.2.0": {
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==", "integrity": "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==",
"dependencies": [
"@tauri-apps/api"
]
},
"@tauri-apps/plugin-shell@2.2.0": {
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
"dependencies": [
"@tauri-apps/api"
]
},
"@tauri-apps/plugin-store@2.2.0": {
"integrity": "sha512-hJTRtuJis4w5fW1dkcgftsYxKXK0+DbAqurZ3CURHG5WkAyyZgbxpeYctw12bbzF9ZbZREXZklPq8mocCC3Sgg==",
"dependencies": [ "dependencies": [
"@tauri-apps/api" "@tauri-apps/api"
] ]
@ -386,24 +410,26 @@
"@types/estree@1.0.6": { "@types/estree@1.0.6": {
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
}, },
"@types/prop-types@15.7.13": { "@types/react-dom@19.0.4_@types+react@19.0.10": {
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
},
"@types/react-dom@18.3.1": {
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
"dependencies": [ "dependencies": [
"@types/react" "@types/react@19.0.10"
] ]
}, },
"@types/react@18.3.12": { "@types/react@19.0.10": {
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dependencies": [ "dependencies": [
"@types/prop-types",
"csstype" "csstype"
] ]
}, },
"@vitejs/plugin-react@4.3.3_vite@5.4.11_@babel+core@7.26.0": { "@types/react@19.0.12": {
"integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
"dependencies": [
"csstype"
]
},
"@vitejs/plugin-react@4.3.4_vite@6.2.2_@babel+core@7.26.10": {
"integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
"dependencies": [ "dependencies": [
"@babel/core", "@babel/core",
"@babel/plugin-transform-react-jsx-self", "@babel/plugin-transform-react-jsx-self",
@ -413,8 +439,8 @@
"vite" "vite"
] ]
}, },
"browserslist@4.24.2": { "browserslist@4.24.4": {
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"dependencies": [ "dependencies": [
"caniuse-lite", "caniuse-lite",
"electron-to-chromium", "electron-to-chromium",
@ -422,8 +448,8 @@
"update-browserslist-db" "update-browserslist-db"
] ]
}, },
"caniuse-lite@1.0.30001680": { "caniuse-lite@1.0.30001706": {
"integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==" "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug=="
}, },
"convert-source-map@2.0.0": { "convert-source-map@2.0.0": {
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
@ -431,17 +457,17 @@
"csstype@3.1.3": { "csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"debug@4.3.7": { "debug@4.4.0": {
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": [ "dependencies": [
"ms" "ms"
] ]
}, },
"electron-to-chromium@1.5.57": { "electron-to-chromium@1.5.122": {
"integrity": "sha512-xS65H/tqgOwUBa5UmOuNSLuslDo7zho0y/lgQw35pnrqiZh7UOWHCeL/Bt6noJATbA6tpQJGCifsFsIRZj1Fqg==" "integrity": "sha512-EML1wnwkY5MFh/xUnCvY8FrhUuKzdYhowuZExZOfwJo+Zu9OsNCI23Cgl5y7awy7HrUHSwB1Z8pZX5TI34lsUg=="
}, },
"esbuild@0.21.5": { "esbuild@0.25.1": {
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dependencies": [ "dependencies": [
"@esbuild/aix-ppc64", "@esbuild/aix-ppc64",
"@esbuild/android-arm", "@esbuild/android-arm",
@ -460,7 +486,9 @@
"@esbuild/linux-riscv64", "@esbuild/linux-riscv64",
"@esbuild/linux-s390x", "@esbuild/linux-s390x",
"@esbuild/linux-x64", "@esbuild/linux-x64",
"@esbuild/netbsd-arm64",
"@esbuild/netbsd-x64", "@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64", "@esbuild/openbsd-x64",
"@esbuild/sunos-x64", "@esbuild/sunos-x64",
"@esbuild/win32-arm64", "@esbuild/win32-arm64",
@ -483,18 +511,12 @@
"js-tokens@4.0.0": { "js-tokens@4.0.0": {
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
}, },
"jsesc@3.0.2": { "jsesc@3.1.0": {
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==" "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="
}, },
"json5@2.2.3": { "json5@2.2.3": {
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
}, },
"loose-envify@1.4.0": {
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": [
"js-tokens"
]
},
"lru-cache@5.1.1": { "lru-cache@5.1.1": {
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dependencies": [ "dependencies": [
@ -504,27 +526,26 @@
"ms@2.1.3": { "ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"nanoid@3.3.7": { "nanoid@3.3.11": {
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
}, },
"node-releases@2.0.18": { "node-releases@2.0.19": {
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
}, },
"picocolors@1.1.1": { "picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"postcss@8.4.49": { "postcss@8.5.3": {
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dependencies": [ "dependencies": [
"nanoid", "nanoid",
"picocolors", "picocolors",
"source-map-js" "source-map-js"
] ]
}, },
"react-dom@18.3.1_react@18.3.1": { "react-dom@19.0.0_react@19.0.0": {
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"dependencies": [ "dependencies": [
"loose-envify",
"react", "react",
"scheduler" "scheduler"
] ]
@ -532,14 +553,11 @@
"react-refresh@0.14.2": { "react-refresh@0.14.2": {
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==" "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="
}, },
"react@18.3.1": { "react@19.0.0": {
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="
"dependencies": [
"loose-envify"
]
}, },
"rollup@4.26.0": { "rollup@4.36.0": {
"integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", "integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==",
"dependencies": [ "dependencies": [
"@rollup/rollup-android-arm-eabi", "@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64", "@rollup/rollup-android-arm64",
@ -551,6 +569,7 @@
"@rollup/rollup-linux-arm-musleabihf", "@rollup/rollup-linux-arm-musleabihf",
"@rollup/rollup-linux-arm64-gnu", "@rollup/rollup-linux-arm64-gnu",
"@rollup/rollup-linux-arm64-musl", "@rollup/rollup-linux-arm64-musl",
"@rollup/rollup-linux-loongarch64-gnu",
"@rollup/rollup-linux-powerpc64le-gnu", "@rollup/rollup-linux-powerpc64le-gnu",
"@rollup/rollup-linux-riscv64-gnu", "@rollup/rollup-linux-riscv64-gnu",
"@rollup/rollup-linux-s390x-gnu", "@rollup/rollup-linux-s390x-gnu",
@ -563,11 +582,8 @@
"fsevents" "fsevents"
] ]
}, },
"scheduler@0.23.2": { "scheduler@0.25.0": {
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
"dependencies": [
"loose-envify"
]
}, },
"semver@6.3.1": { "semver@6.3.1": {
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
@ -575,19 +591,19 @@
"source-map-js@1.2.1": { "source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
}, },
"typescript@5.6.3": { "typescript@5.8.2": {
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==" "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="
}, },
"update-browserslist-db@1.1.1_browserslist@4.24.2": { "update-browserslist-db@1.1.3_browserslist@4.24.4": {
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dependencies": [ "dependencies": [
"browserslist", "browserslist",
"escalade", "escalade",
"picocolors" "picocolors"
] ]
}, },
"vite@5.4.11": { "vite@6.2.2": {
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"dependencies": [ "dependencies": [
"esbuild", "esbuild",
"fsevents", "fsevents",
@ -602,16 +618,18 @@
"workspace": { "workspace": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@tauri-apps/api@2", "npm:@tauri-apps/api@^2.3.0",
"npm:@tauri-apps/cli@2", "npm:@tauri-apps/cli@^2.3.1",
"npm:@tauri-apps/plugin-shell@2", "npm:@tauri-apps/plugin-dialog@^2.2.0",
"npm:@types/react-dom@^18.2.7", "npm:@tauri-apps/plugin-shell@^2.2.0",
"npm:@types/react@^18.2.15", "npm:@tauri-apps/plugin-store@^2.2.0",
"npm:@vitejs/plugin-react@^4.2.1", "npm:@types/react-dom@^19.0.4",
"npm:react-dom@^18.2.0", "npm:@types/react@^19.0.12",
"npm:react@^18.2.0", "npm:@vitejs/plugin-react@^4.3.4",
"npm:typescript@^5.2.2", "npm:react-dom@19",
"npm:vite@^5.3.1" "npm:react@19",
"npm:typescript@^5.8.2",
"npm:vite@^6.2.2"
] ]
} }
} }

View File

@ -10,17 +10,19 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"react": "^18.2.0", "@tauri-apps/plugin-dialog": "^2.2.0",
"react-dom": "^18.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/api": "^2", "react": "^19.0.0",
"@tauri-apps/plugin-shell": "^2" "react-dom": "^19.0.0",
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.15", "@types/react": "^19.0.12",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.2.2", "typescript": "^5.8.2",
"vite": "^5.3.1", "vite": "^6.2.2",
"@tauri-apps/cli": "^2" "@tauri-apps/cli": "^2.3.1"
} }
} }

View File

@ -3,7 +3,7 @@ name = "lanspread-tauri-deno-ts"
version = "0.1.0" version = "0.1.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -25,11 +25,15 @@ lanspread-mdns = { path = "../../lanspread-mdns" }
# external # external
eyre = { workspace = true } eyre = { workspace = true }
log = "0.4" log = { workspace = true }
tauri = { version = "2", features = [] } serde = { workspace = true }
tauri-plugin-log = "2" serde_json = { workspace = true }
tauri-plugin-shell = "2" tauri = { workspace = true }
serde = { version = "1", features = ["derive"] } tauri-plugin-log = { workspace = true }
serde_json = "1" tauri-plugin-shell = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-store = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }

View File

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

View File

@ -1,15 +1,26 @@
use std::net::SocketAddr; use std::{
collections::HashSet,
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
use eyre::bail;
use lanspread_client::{ClientCommand, ClientEvent}; use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE}; use lanspread_db::db::{Game, GameDB};
use tauri::{AppHandle, Emitter as _, Listener as _, Manager}; use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service};
use tokio::sync::{mpsc::UnboundedSender, Mutex}; use tauri::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{ShellExt, process::Command};
use tokio::sync::{Mutex, RwLock, mpsc::UnboundedSender};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
struct LanSpreadState { struct LanSpreadState {
server_addr: Mutex<Option<SocketAddr>>, server_addr: RwLock<Option<SocketAddr>>,
client_ctrl: UnboundedSender<ClientCommand>, client_ctrl: UnboundedSender<ClientCommand>,
games: Arc<RwLock<GameDB>>,
games_in_download: Arc<Mutex<HashSet<String>>>,
games_folder: Arc<RwLock<String>>,
} }
#[tauri::command] #[tauri::command]
@ -22,22 +33,202 @@ fn request_games(state: tauri::State<LanSpreadState>) {
} }
#[tauri::command] #[tauri::command]
fn run_game_backend(id: String, state: tauri::State<LanSpreadState>) -> String { fn install_game(id: String, state: tauri::State<LanSpreadState>) -> bool {
log::error!("Running game with id {id}"); let already_in_download = tauri::async_runtime::block_on(async {
if state.inner().games_in_download.lock().await.contains(&id) {
log::warn!("Game is already downloading: {id}");
return true;
}
false
});
// let result = Command::new(r#"C:\Users\ddidderr\scoop\apps\mpv\0.39.0\mpv.exe"#).spawn(); if already_in_download {
return false;
}
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) { if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) {
log::error!("Failed to send message to client: {e:?}"); log::error!("Failed to send message to client: {e:?}");
} }
"TODO".to_string() true
}
// if result.is_ok() { #[cfg(target_os = "windows")]
// "Ok".to_string() fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
// } else { use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
// "Failed to run game".to_string()
// } use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR};
let file_wide: Vec<u16> = OsStr::new(file).encode_wide().chain(Some(0)).collect();
let params_wide: Vec<u16> = OsStr::new(params).encode_wide().chain(Some(0)).collect();
let dir_wide: Vec<u16> = OsStr::new(dir).encode_wide().chain(Some(0)).collect();
let runas_wide: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
let result = unsafe {
ShellExecuteW(
None,
PCWSTR::from_raw(runas_wide.as_ptr()),
PCWSTR::from_raw(file_wide.as_ptr()),
PCWSTR::from_raw(params_wide.as_ptr()),
PCWSTR::from_raw(dir_wide.as_ptr()),
windows::Win32::UI::WindowsAndMessaging::SW_HIDE,
)
};
(result.0 as usize) > 32 // Success if greater than 32
}
#[cfg(target_os = "windows")]
fn run_game_windows(id: String, state: tauri::State<LanSpreadState>) {
use std::fs::File;
const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done";
let games_folder =
tauri::async_runtime::block_on(async { state.inner().games_folder.read().await.clone() });
let games_folder = PathBuf::from(games_folder);
if !games_folder.exists() {
log::error!("games_folder {games_folder:?} does not exist");
return;
}
let game_path = games_folder.join(id.clone());
let game_setup_bin = game_path.join("game_setup.cmd");
let game_start_bin = game_path.join("game_start.cmd");
let first_start_done_file = game_path.join(FIRST_START_DONE_FILE);
if !first_start_done_file.exists() && game_setup_bin.exists() {
let result = run_as_admin(
"cmd.exe",
&format!(
r#"/c "{} local {} de playername""#,
game_setup_bin.display(),
&id
),
&game_path.display().to_string(),
);
if !result {
log::error!("failed to run game_setup.cmd");
return;
}
if let Err(e) = File::create(&first_start_done_file) {
log::error!("failed to create {first_start_done_file:?}: {e}");
}
}
if game_start_bin.exists() {
let result = run_as_admin(
"cmd.exe",
&format!(
r#"/c "{} local {} de playername""#,
game_start_bin.display(),
&id
),
&game_path.display().to_string(),
);
if !result {
log::error!("failed to run game_start.cmd");
}
}
}
#[tauri::command]
fn run_game(id: String, state: tauri::State<LanSpreadState>) {
#[cfg(target_os = "windows")]
run_game_windows(id, state);
#[cfg(not(target_os = "windows"))]
{
let _ = state;
log::error!("run_game not implemented for this platform: id={id}");
}
}
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::debug!("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) {
log::info!("update_game_directory: {path}");
app_handle
.state::<LanSpreadState>()
.client_ctrl
.send(ClientCommand::SetGameDir(path.clone()))
.unwrap();
{
tauri::async_runtime::block_on(async {
let mut games_folder = app_handle
.state::<LanSpreadState>()
.inner()
.games_folder
.write()
.await;
*games_folder = path.clone();
});
}
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
.write()
.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("version.ini").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) { async fn find_server(app: AppHandle) {
@ -48,15 +239,12 @@ async fn find_server(app: AppHandle) {
Ok(server_addr) => { Ok(server_addr) => {
log::info!("Found server at {server_addr}"); log::info!("Found server at {server_addr}");
let state: tauri::State<LanSpreadState> = app.state(); let state: tauri::State<LanSpreadState> = app.state();
{ *state.server_addr.write().await = Some(server_addr);
// mutex scope
let mut addr = state.server_addr.lock().await;
*addr = Some(server_addr);
}
state state
.client_ctrl .client_ctrl
.send(ClientCommand::ServerAddr(server_addr)) .send(ClientCommand::ServerAddr(server_addr))
.unwrap(); .unwrap();
request_games(state);
break; break;
} }
Err(e) => { Err(e) => {
@ -66,6 +254,89 @@ 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
*state.games.write().await = 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");
}
}
fn add_final_slash(path: &str) -> String {
#[cfg(target_os = "windows")]
const SLASH_CHAR: char = '\\';
#[cfg(not(target_os = "windows"))]
const SLASH_CHAR: char = '/';
if path.ends_with(SLASH_CHAR) {
path.to_string()
} else {
format!("{path}{SLASH_CHAR}")
}
}
async fn do_unrar(sidecar: Command, rar_file: &Path, dest_dir: &Path) -> eyre::Result<()> {
if let Ok(()) = std::fs::create_dir_all(dest_dir) {
if let Ok(rar_file) = rar_file.canonicalize() {
if let Ok(dest_dir) = dest_dir.canonicalize() {
let dest_dir = dest_dir
.to_str()
.ok_or_else(|| eyre::eyre!("failed to get str of dest_dir"))?;
log::info!(
"unrar game: {} to {}",
rar_file.canonicalize()?.display(),
dest_dir
);
let out = sidecar
.arg("x") // extract files
.arg(rar_file.canonicalize()?)
.arg("-y") // Assume Yes on all queries
.arg("-o") // Set overwrite mode
.arg(add_final_slash(dest_dir))
.output()
.await?;
if !out.status.success() {
log::error!("unrar stderr: {}", String::from_utf8_lossy(&out.stderr));
}
return Ok(());
} else {
log::error!("dest_dir canonicalize failed: {:?}", &dest_dir);
}
} else {
log::error!("rar_file canonicalize failed: {:?}", &rar_file);
}
} else {
log::error!("failed to create dest_dir: {:?}", &dest_dir);
}
bail!("failed to create directory: {dest_dir:?}");
}
async fn unpack_game(id: &str, sidecar: Command, games_folder: String) {
let game_path = PathBuf::from(games_folder).join(id);
let eti_rar = game_path.join(format!("{id}.eti"));
let local_path = game_path.join("local");
if let Err(e) = do_unrar(sidecar, &eti_rar, &local_path).await {
log::error!("{eti_rar:?} -> {local_path:?}: {e}");
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let tauri_logger_builder = tauri_plugin_log::Builder::new() let tauri_logger_builder = tauri_plugin_log::Builder::new()
@ -74,8 +345,6 @@ pub fn run() {
tauri_plugin_log::TargetKind::Stdout, tauri_plugin_log::TargetKind::Stdout,
)) ))
.level(log::LevelFilter::Info) .level(log::LevelFilter::Info)
.level_for("lanspread_client", log::LevelFilter::Debug)
.level_for("lanspread_tauri_leptos_lib", log::LevelFilter::Debug)
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off); .level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
// channel to pass commands to the client // channel to pass commands to the client
@ -87,14 +356,24 @@ pub fn run() {
tokio::sync::mpsc::unbounded_channel::<ClientEvent>(); tokio::sync::mpsc::unbounded_channel::<ClientEvent>();
let lanspread_state = LanSpreadState { let lanspread_state = LanSpreadState {
server_addr: Mutex::new(None), server_addr: RwLock::new(None),
client_ctrl: tx_client_control, client_ctrl: tx_client_control,
games: Arc::new(RwLock::new(GameDB::empty())),
games_in_download: Arc::new(Mutex::new(HashSet::new())),
games_folder: Arc::new(RwLock::new("".to_string())),
}; };
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_logger_builder.build()) .plugin(tauri_logger_builder.build())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![run_game_backend, request_games]) .invoke_handler(tauri::generate_handler![
request_games,
install_game,
run_game,
update_game_directory
])
.manage(lanspread_state) .manage(lanspread_state)
.setup(|app| { .setup(|app| {
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
@ -109,18 +388,92 @@ pub fn run() {
while let Some(event) = rx_client_event.recv().await { while let Some(event) = rx_client_event.recv().await {
match event { match event {
ClientEvent::ListGames(games) => { ClientEvent::ListGames(games) => {
log::debug!("Received client event: ListGames"); log::info!("ClientEvent::ListGames received");
update_game_db(games, app_handle.clone()).await;
}
ClientEvent::GotGameFiles { id, file_descriptions } => {
log::info!("ClientEvent::GotGameFiles received");
for game in &games { if let Err(e) = app_handle.emit(
log::trace!("client event ListGames iter: {game:?}"); "game-download-pre",
Some(id.clone()),
) {
log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}");
} }
if let Err(e) = app_handle.emit("games-list-updated", Some(games)) { app_handle
log::error!("Failed to emit games-list-updated event: {e}"); .state::<LanSpreadState>()
} else { .inner()
log::info!("Emitted games-list-updated event"); .client_ctrl
.send(ClientCommand::DownloadGameFiles{
id,
file_descriptions,
})
.unwrap();
}
ClientEvent::DownloadGameFilesBegin { id } => {
log::info!("ClientEvent::DownloadGameFilesBegin received");
app_handle
.state::<LanSpreadState>()
.inner()
.games_in_download
.lock()
.await
.insert(id.clone());
if let Err(e) = app_handle.emit("game-download-begin", Some(id)) {
log::error!("ClientEvent::DownloadGameFilesBegin: Failed to emit game-download-begin event: {e}");
} }
} }
ClientEvent::DownloadGameFilesFinished { id } => {
log::info!("ClientEvent::DownloadGameFilesFinished received");
if let Err(e) = app_handle.emit("game-download-finished", Some(id.clone())) {
log::error!("ClientEvent::DownloadGameFilesFinished: Failed to emit game-download-finished event: {e}");
}
app_handle
.state::<LanSpreadState>()
.inner()
.games_in_download
.lock()
.await
.remove(&id.clone());
let games_folder = app_handle
.state::<LanSpreadState>()
.inner()
.games_folder
.read()
.await
.clone();
if let Ok(sidecar) = app_handle.shell().sidecar("unrar") {
unpack_game(&id, sidecar, games_folder).await;
log::info!("ClientEvent::UnpackGameFinished received");
if let Err(e) = app_handle.emit("game-unpack-finished", Some(id.clone())) {
log::error!("ClientEvent::UnpackGameFinished: Failed to emit game-unpack-finished event: {e}");
}
}
}
ClientEvent::DownloadGameFilesFailed { id } => {
log::warn!("ClientEvent::DownloadGameFilesFailed received");
if let Err(e) = app_handle.emit("game-download-failed", Some(id.clone())) {
log::error!("Failed to emit game-download-failed event: {e}");
}
app_handle
.state::<LanSpreadState>()
.inner()
.games_in_download
.lock()
.await
.remove(&id.clone());
},
} }
} }
}); });

View File

@ -1,8 +1,8 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "lanspread-tauri-deno-ts", "productName": "softlan-launcher",
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.lanspread-tauri-deno-ts.app", "identifier": "com.softlan-launcher.app",
"build": { "build": {
"beforeDevCommand": "deno task dev", "beforeDevCommand": "deno task dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
@ -12,9 +12,9 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "lanspread-tauri-deno-ts", "title": "softlan-launcher",
"width": 800, "width": 1526,
"height": 600 "height": 1120
} }
], ],
"security": { "security": {
@ -30,6 +30,9 @@
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
],
"externalBin": [
"binaries/unrar"
] ]
} }
} }

View File

@ -102,13 +102,6 @@ h1.align-center {
box-shadow: 0 8px 15px rgba(0, 191, 255, 0.2); 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 { .play-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9); background: linear-gradient(45deg, #09305a, #4866b9);
box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6); box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6);
@ -124,7 +117,6 @@ h1.align-center {
} }
.search-container { .search-container {
padding: 20px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
@ -150,3 +142,45 @@ h1.align-center {
.search-input::placeholder { .search-input::placeholder {
color: #8892b0; 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,154 @@
import {useEffect, useState} from 'react'; import { useEffect, useState } from 'react';
import {invoke} from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import {listen} from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog';
import { load } from '@tauri-apps/plugin-store';
import "./App.css"; import "./App.css";
const FILE_STORAGE = 'launcher-settings.json';
const GAME_DIR_KEY = 'game-directory';
// enum with install status
enum InstallStatus {
NotInstalled = 'NotInstalled',
CheckingServer = 'CheckingServer',
Downloading = 'Downloading',
Unpacking = 'Unpacking',
Installed = 'Installed',
}
interface Game { interface Game {
id: string; id: string;
name: string; name: string;
description: string; description: string;
size: number; size: number;
thumbnail: Uint8Array; thumbnail: Uint8Array;
installed: boolean;
install_status: InstallStatus;
} }
const App = () => { const App = () => {
const [gameItems, setGameItems] = useState<Game[]>([]); const [gameItems, setGameItems] = useState<Game[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [gameDir, setGameDir] = useState('');
const filteredGames = gameItems.filter(item => const filteredGames = gameItems.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) 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(() => {
// Listen for game-download-failed events specifically
const setupDownloadFailedListener = async () => {
const unlisten = await listen('game-download-failed', (event) => {
const game_id = event.payload as string;
console.log(`❌ game-download-failed ${game_id} event received`);
setGameItems(prev => prev.map(item => item.id === game_id
? {...item, install_status: InstallStatus.NotInstalled}
: item));
// Convert to string explicitly and verify it's not empty
const pathString = String(gameDir);
if (!pathString) {
console.error('gameDir is empty before invoke!');
return;
}
invoke('update_game_directory', { path: pathString })
.catch(error => console.error('❌ Error updating game directory:', error));
});
return unlisten;
};
setupDownloadFailedListener();
}, [gameDir]);
useEffect(() => {
// Listen for game-unpack-finished events specifically
const setupUnpackListener = async () => {
const unlisten = await listen('game-unpack-finished', (event) => {
const game_id = event.payload as string;
console.log(`🗲 game-unpack-finished ${game_id} event received`);
setGameItems(prev => prev.map(item => item.id === game_id
? {...item, install_status: InstallStatus.Installed}
: item));
// Convert to string explicitly and verify it's not empty
const pathString = String(gameDir);
if (!pathString) {
console.error('gameDir is empty before invoke!');
return;
}
invoke('update_game_directory', { path: pathString })
.catch(error => console.error('❌ Error updating game directory:', error));
});
return unlisten;
};
setupUnpackListener();
}, [gameDir]);
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(() => { useEffect(() => {
console.log('🔵 Effect starting - setting up listener and requesting games'); console.log('🔵 Effect starting - setting up listener and requesting games');
let isSubscribed = true; // For tracking if component is still mounted
const setupEventListener = async () => { const setupEventListener = async () => {
try { try {
const unlisten = await listen('games-list-updated', (event) => { // Listen for games-list-updated events
if (!isSubscribed) return; // Don't update state if unmounted const unlisten_games = await listen('games-list-updated', (event) => {
console.log('🗲 Received games-list-updated event');
console.log('📥 Received games-list-updated event');
const games = event.payload as Game[]; const games = event.payload as Game[];
games.forEach(game => { console.log(`🎮 ${games.length} Games received`);
console.log(`🎮 game: ${JSON.stringify(game.id)}`);
});
setGameItems(games); setGameItems(games);
getInitialGameDir();
});
// Listen for game-download-begin events
const unlisten_game_download_begin = await listen('game-download-begin', (event) => {
const game_id = event.payload as string;
console.log(`🗲 game-download-begin ${game_id} event received`);
setGameItems(prev => prev.map(item => item.id === game_id
? { ...item, install_status: InstallStatus.Downloading }
: item));
});
// Listen for game-download-finished events
const unlisten_game_download_finished = await listen('game-download-finished', (event) => {
const game_id = event.payload as string;
console.log(`🗲 game-download-finished ${game_id} event received`);
setGameItems(prev => prev.map(item => item.id === game_id
? { ...item, install_status: InstallStatus.Unpacking }
: item));
}); });
// Initial request for games // Initial request for games
@ -44,8 +158,9 @@ const App = () => {
// Cleanup function // Cleanup function
return () => { return () => {
console.log('🧹 Cleaning up - removing listener'); console.log('🧹 Cleaning up - removing listener');
isSubscribed = false; unlisten_games();
unlisten(); unlisten_game_download_begin();
unlisten_game_download_finished();
}; };
} catch (error) { } catch (error) {
console.error('❌ Error in setup:', error); console.error('❌ Error in setup:', error);
@ -57,26 +172,58 @@ const App = () => {
// Cleanup // Cleanup
return () => { return () => {
console.log('🚫 Effect cleanup - component unmounting'); console.log('🚫 Effect cleanup - component unmounting');
isSubscribed = false;
}; };
}, []); // Empty dependency array means this runs once on mount }, []); // Empty dependency array means this runs once on mount
const runGame = async (id: string) => { const runGame = async (id: string) => {
console.log(`🎯 Running game with id=${id}`); console.log(`🎯 Running game with id=${id}`);
try { try {
const result = await invoke('run_game_backend', {id}); const result = await invoke('run_game', { id });
console.log(`✅ Game started, result=${result}`); console.log(`✅ Game started, result=${result}`);
} catch (error) { } catch (error) {
console.error('❌ Error running game:', error); console.error('❌ Error running game:', error);
} }
}; };
const installGame = async (id: string) => {
console.log(`🎯 Installing game with id=${id}`);
try {
const success = await invoke('install_game', { id });
if (success) {
console.log(`✅ Game install for id=${id} started...`);
// update install status in gameItems for this game
setGameItems(prev => prev.map(item => item.id === id
? { ...item, install_status: InstallStatus.CheckingServer }
: item));
} else {
// game is already being installed
console.warn(`🚧 Game with id=${id} is already being installed`);
}
} catch (error) {
console.error('❌ Error installing game:', error);
}
};
const dialogGameDir = async () => {
const file = await open({
multiple: false,
directory: true,
});
if (file) {
setGameDir(file);
}
};
// Rest of your component remains the same // Rest of your component remains the same
return ( return (
<main className="container"> <main className="container">
<div className="fixed-header"> <div className="fixed-header">
<h1 className="align-center">SoftLAN Launcher</h1> <h1 className="align-center">SoftLAN Launcher</h1>
<div className="main-header"> <div className="main-header">
{gameItems.length > 0 ? (
<div className="search-settings-wrapper">
<div></div>
<div className="search-container"> <div className="search-container">
<input <input
type="text" type="text"
@ -86,6 +233,16 @@ const App = () => {
className="search-input" className="search-input"
/> />
</div> </div>
<div className="settings-container">
<button onClick={dialogGameDir} className="settings-button">Set Game Directory</button>
<span className="settings-text">{gameDir}</span>
</div>
</div>
) : (
<div className="search-container">
Waiting for connection to server...
</div>
)}
</div> </div>
</div> </div>
<div className="grid-container"> <div className="grid-container">
@ -94,14 +251,23 @@ const App = () => {
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), ''); const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`; const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`;
return ( return (
<div key={item.id} className="item" onClick={() => runGame(item.id)}> <div key={item.id} className="item">
<img src={thumbnailUrl} alt={`${item.name} thumbnail`} /> <img src={thumbnailUrl} alt={`${item.name} thumbnail`} />
<div className="item-name">{item.name}</div> <div className="item-name">{item.name}</div>
<div className="description"> <div className="description">
<span className="desc-text">{item.description.slice(0, 10)}</span> <span className="desc-text">{item.description.slice(0, 10)}</span>
<span className="size-text">{item.size.toString()}</span> <span className="size-text">{(item.size / 1024 / 1024 / 1024).toFixed(1)} GB</span>
</div>
<div className="play-button"
onClick={() => item.installed
? runGame(item.id)
: installGame(item.id)}>
{item.installed ? 'Play'
: item.install_status === InstallStatus.CheckingServer ? 'Checking server...'
: item.install_status === InstallStatus.Downloading ? 'Downloading...'
: item.install_status === InstallStatus.Unpacking ? 'Unpacking...'
: 'Install'}
</div> </div>
<div className="play-button">Play</div>
</div> </div>
); );
})} })}

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-utils" name = "lanspread-utils"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -1,6 +1,8 @@
#[macro_export] #[macro_export]
macro_rules! maybe_addr { macro_rules! maybe_addr {
($addr:expr) => { ($addr:expr) => {
$addr.map_or("<unknown>".to_string(), |addr| addr.to_string()) $addr.map_or(Arc::new("<unknown>".to_string()), |addr| {
Arc::new(addr.to_string())
})
}; };
} }

100
key.pem
View File

@ -1,52 +1,52 @@
-----BEGIN PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
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
4g8cERBIkBP2otGtuFoA4oIQ6qsh IKQxaY3Y5s7XzzFJ1Tg3RjHekKHcVw==
-----END PRIVATE KEY----- -----END PRIVATE KEY-----

View File

@ -1,6 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export RUST_LOG=lanspread_mdns=debug,lanspread_db=debug,lanspread_server=debug,lanspread_proto=debug export RUST_LOG=info,lanspread=debug
exec cargo run -p lanspread-server -- "$@" exec cargo run -p lanspread-server -- "$@"
#RUST_LOG=info exec cargo run --profile release-lto -p lanspread-server

View File

@ -1,24 +0,0 @@
$Env:RUST_LOG = "info,lanspread_client=debug,lanspread_proto=debug"
# Start the process with redirected standard input
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.FileName = "cargo"
$processInfo.Arguments = "run -p lanspread-client -- $args"
$processInfo.UseShellExecute = $false
$processInfo.RedirectStandardInput = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
$process.Start() | Out-Null
# Continuously write commands to the standard input of the process
while (!$process.HasExited) {
Start-Sleep -Milliseconds 100
$process.StandardInput.WriteLine("list")
Start-Sleep -Milliseconds 100
$process.StandardInput.WriteLine("get 1")
Start-Sleep -Milliseconds 100
$process.StandardInput.WriteLine("get 25")
}
$process.WaitForExit()