Compare commits

..

46 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
009c0739d2 [ui] make the grid good 2024-11-14 00:01:55 +01:00
a6ed6e04fe [backup] games from server with images 2024-11-13 23:51:28 +01:00
5d45c4ce4b [ui] change framework from Tauri Leptos to Tauri Vanilla (Typescript) and React 2024-11-13 22:15:04 +01:00
ff0cee58d5 [logging] tiny improvements, still the WASM stacktrace error 2024-11-12 22:56:49 +01:00
1388bc2115 [feat] use eti game.db, commit not working, something is wrong with game.id in the client/frontend 2024-11-12 22:12:49 +01:00
ba2177abf0 [docs] README.md added 2024-11-10 20:51:40 +01:00
4d9051aece [docs] add a lessons learned document 2024-11-10 18:07:59 +01:00
5ef77addbf [fix] SotLAN -> SoftLAN 2024-11-10 18:07:41 +01:00
85 changed files with 9652 additions and 2885 deletions

2
.gitignore vendored
View File

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

3658
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,50 @@
[workspace]
members = [
"crates/lanspread-compat",
"crates/lanspread-db",
"crates/lanspread-utils",
"crates/lanspread-mdns",
"crates/lanspread-proto",
"crates/lanspread-server",
"crates/lanspread-client",
"crates/lanspread-tauri-leptos",
"crates/lanspread-tauri-leptos/src-tauri",
"crates/lanspread-tauri-deno-ts/src-tauri",
]
resolver = "2"
[workspace.dependencies]
bytes = "1.8"
clap = { version = "4.5", features = ["derive"] }
bytes = { version = "1", features = ["serde"] }
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
eyre = "0.6"
itertools = "0.13"
s2n-quic = { version = "1.49", features = ["provider-event-tracing"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.41", features = ["full"] }
gethostname = "1"
itertools = "0.14"
log = "0.4"
mdns-sd = "0.13"
s2n-quic = { version = "1", features = ["provider-event-tracing"] }
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-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]
debug = true

15
LESSONS_LEARNED.md Normal file
View File

@ -0,0 +1,15 @@
# Wrong decisions
- **tauri-leptos** adds unnecessary complexity to the project.
Tauri is built to transfer backend Rust stuff to the frontend JavaScript world.
But with leptos, the frontend becomes Rust so you have to transfer everything again back into the Rust world.
# Good decisions
- **Tauri** is strong 💪
- Easy project setup with `cargo install create-tauri-app --locked` and `cargo create-tauri-app`
- Easy testing with `cargo tauri dev`
- Easy bundling (with installers and everything) with `cargo tauri build`
- Final binary size of my tauri-leptos project is 13MB (which seems small to me if you think that a whole WASM / Web stack is in there)
# Open questions
- **logging**: I don't understand the relationship (or lack thereof) between `tracing` and `log`.
I had to refactor logging in the client away from `tracing`.

45
README.md Normal file
View File

@ -0,0 +1,45 @@
# lanspread
## Description
Simple server and GUI for LAN parties.
## Development
### Prerequisites
```bash
# install Tauri CLI
cargo install tauri-cli
# install Deno with a package manager or from https://deno.land/
```
### Build
#### Frontend
```bash
# Development
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
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
```bash
# Development
./server.sh [options...] # prefix with RUST_LOG=your_module=debug or similary for more verbose output
# Production
cargo build --profile release-lto -p lanspread-server
```

View File

@ -1,35 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCTCCA/GgAwIBAgIUJKXDdTgaLqvRC86wusXKd1tX+q4wDQYJKoZIhvcNAQEL
MIIGCTCCA/GgAwIBAgIUDpGo26xcNO0Pw/L8axkDSVCnupcwDQYJKoZIhvcNAQEL
BQAwgZUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl
cmxpbjESMBAGA1UECgwJUGF1bG9zb2Z0MREwDwYDVQQLDAhTb2Z0d2FyZTEkMCIG
CSqGSIb3DQEJARYVZGRpZGRlcnJAcGF1bC5uZXR3b3JrMRcwFQYDVQQDDA5NYXN0
ZXJEZXNhc3RlcjAeFw0yNDAyMDMxNDI4MzNaFw0yNTAyMDIxNDI4MzNaMIGVMQsw
ZXJEZXNhc3RlcjAeFw0yNTAzMDIxMjM0MzRaFw0yNjAzMDIxMjM0MzRaMIGVMQsw
CQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEjAQ
BgNVBAoMCVBhdWxvc29mdDERMA8GA1UECwwIU29mdHdhcmUxJDAiBgkqhkiG9w0B
CQEWFWRkaWRkZXJyQHBhdWwubmV0d29yazEXMBUGA1UEAwwOTWFzdGVyRGVzYXN0
ZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQClHI7EI1cAO40AOooG
FxqCxw7f3yjv4yGkEjx5Xk5zyURxrspMG1MbFfHGVlhnpe0vTMzMFwrrl7FeQ2+T
wmypwqrGZs009DP/CFFG8BX7EBX2SKQzFzUdWWYTwHtPtw8RHD+Aee0UA8QH9Y/o
koKYysja1ahyOmbk7lg7B+igTZKipXFyigX/H3iK1n2gJU8gBlQcRfyI8VTYfTjA
k9hllluzycOuKSAQXrVCArbQfTUl2edd4t0tmvjear0wV2PclVm+8AXnsSKkg3Cj
+0mdDvTKVxkycCTJwGa7iMe3OchFaGqGwRBRnPr6lbqNjmI0CwK+cNcwxEDcSos8
UtyQ6amCpLS0fhsLL1SyPqvj8toGRr0cSaM4suMx24Pl7WXItCdW/dijteqe/p41
BTtMTNXBjsCl26oTE8FIQHbxzFhVkKVGV6UyHrkmdjyGmdwLqt5+ks2eaVrW3Pld
yw0h0kWe2KmK+A/95QpZA7KNlYrkEi2M1cZZXmiQZh3uOWBb9P94zQ262tz+jStQ
hkFfHSmFqXSOgyRc7VA/6CkR/qStWWwMcgLJcjnmQ0ixtaW6zrObHZBD+72Q3lHm
0m11mc55tP44UadNY8jjlLWrL3xq4txMyqvwWaE0xIdzec0Oj5Gu/aPivym3BYY5
AiGHO4P7x53k3zr118hsMQi4WQIDAQABo08wTTAJBgNVHRMEAjAAMAsGA1UdDwQE
AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFFg0Y/xYk6IdBXDN
cSOIoHxrNiecMA0GCSqGSIb3DQEBCwUAA4ICAQBjvz66og+q1n9mwN7vxuolosJV
99x9kg/sf0RTdwkAVuphZrwIoEXskGywsRmzAdnOZXCWf5pO14l/eC782dEDQA7M
+0ap4i8iMUZMFAFbAK52wLvS5tCwuNnm+zqPa1tjyfs4VVGmO9jIQk5+P97NRJzY
vBT676SVZLnOs5DD2FdOCbZrPgvuOlfpcdQIE5ufxJ7en2RObYe8VPjnQb7zp4WE
RClK10xSjv6onwpyDZu7t9Sk9JtnH14jMPeRWw58SPYrcvElpvwvlewlCsWriWgX
Mb/1ME1ZajvOGwfMnanDM2uBRjb2pdvOogpLEhgSK4UsH9MaQleHzez32qK5Xl//
5adttoTr7P48vGJsSNRcG1taPGE2Mv+qHVw7IX2Md6ekWy6L/lTLhH/nbf5QNQTU
QANCKrsYQNbneF598mXAX3Z4nkwVyCpXFwzOSEaqHkuuQclnCZhNb8HrB6U5c4aj
MX7IPaRS7qS27rXcn6WskvPehERb6nhZiz8YiDqdX0ZhaDqflvhOIqzVmx6AY5we
FvEwLfSmVzw0hE/R3DgH0MH8QTUZSLe0gQwyZmqq+3gJv4R7l7DbmObKQneO68Kq
ocNT1cDXUZzAdqkOVZpyEiuI1cTVkXBmJj75DdpMZW4wHkl19APu/jwt60sw9f08
xFR/TSuDnG5jObpzew==
ZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0DbMTTEoUKT9ZNLuR
DKGG84TLu5XauGJiQPoJhnIinSyC/ods0pvmjjcxrrsw39tt6JFb8yz2S6LX3wwL
bwgOPhV3BLzDZYqzbMidequSppsodcKNnZ901SwiNrF2OK1ZXORuUoM4AJXdz5qJ
ZTLyiKOUwQB3obME1cA1zIrBhHiRfXtFTwV+dnwb6L696Nz1z/vxmfzkO95vNppp
0gpz7qd9NoKqOHMSPCIhxrVcw31IGscUWGqEbEiK1Q12YHqxTpHs7ITB4xGbiWdR
ERwZoV3SyAAg+YgHLtGrKOGgxw/E+L6eDwIC2NUjq+hlORWbUfV8i71fOMvZS0eA
7hUIDJopZ/2xEYHCo1793HY9rLDAo2AippvAMG/BmTi2nnrpb2QOFFCNh/qOn+VU
wGDNAqrpNsNgk5MQQBtAjeyG4mzkK8sCYI3H+YVEZ6YMA7GkYBqfiPiFCtdop1TE
IVgo5RkAl4LpbRO03bm65QV5GEKgsWoz7wgGxaYmnjNPoHa4bdEXZe+3W2J6VNba
i9sXwf/E9A6axNDjy1o0fLkK/IFomk0zHzsVxy4HzBBR/8IqEz8bRt/Qy1QbBP9I
0xNallACSHtM2AdsrAkNCrcpaNdrQ14J2cGn5fvRcxKar5bzrDwHcX2pn76wdMMz
1b3io+FolYI/aFeqb9hPX5V+7wIDAQABo08wTTAJBgNVHRMEAjAAMAsGA1UdDwQE
AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNX3XPuVanpmZ73j
btjaeqiALr6UMA0GCSqGSIb3DQEBCwUAA4ICAQAGxfcR8y9/jCudqpCpOthHW01+
12hlPa5CYgRpXpr5n2E/O87+Cq/day7S8gcXKiDBCyhw19HFl8O0b0D0vHfwZFu3
aN4uj86moMROde1CJT59u86+D0EwjInDS126BeKtxovqWh1RPcOdHp4oB8F5IxCT
UiNXqgj9nsL7GoU4yqdQ3TvtJKU8EbsglnBK9/FxeTg5xp38H4FWcwcI5oGVLKBb
JvRI7YluoiYh934K+HMZI+mSpZut5Y5+QSwqy5xMyCbZ6AkVE4CUCGDMn1aUvRNl
sO1vE9aDkn9++Kp0XDNHoPJXZZ/c5EVLdWcmuVzErLWkonfqTHgZ8kbdeBMrTsNt
TaZ0gIQDf+W0idOxhJLIawqv8GK14PuG/a8LlIh37qS3sHHy5aY7lCxI/Ui+Hxhr
Mnjf7roDhjotgUtHsMQGxmXsCMJmVFW6NjtX4b1ZVQRtxionpNo/fzdqJRuaLt5E
uOktXbMPbbRdCyUzgGdxX/iz55osvBdJQCcDVdjiq6QEjo+vdBJTr4QDMQs4Q1p0
OtZswBn34f9O38YlJRLemGhP0ZT0XRTyPJOVU0V1eBRgrab6mXTQMThGAdAkfIIG
rWLZZd7TTBpCfIZS+ZYxvX0+unUej1ZmaRwM3uo7GusDj+hC0Um9gF/d2dgRF6PI
Hy68pcpWun7JH4rZpQ==
-----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]
name = "lanspread-client"
version = "0.1.0"
edition = "2021"
edition = "2024"
[lints.rust]
unsafe_code = "forbid"
@ -16,10 +16,12 @@ unwrap_used = "warn"
lanspread-db = { path = "../lanspread-db" }
lanspread-proto = { path = "../lanspread-proto" }
lanspread-utils = { path = "../lanspread-utils" }
# external
bytes = { workspace = true }
clap = { workspace = true }
eyre = { workspace = true }
log = "0.4"
log = { workspace = true }
s2n-quic = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

View File

@ -1,29 +1,174 @@
use std::{net::SocketAddr, time::Duration};
#![allow(clippy::missing_errors_doc)]
use lanspread_db::db::Game;
use std::{fs::File, io::Write as _, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use bytes::BytesMut;
use lanspread_db::db::{Game, GameFileDescription};
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits};
use tokio::{
io::AsyncWriteExt,
sync::{
RwLock,
mpsc::{UnboundedReceiver, UnboundedSender},
},
};
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
#[derive(Debug)]
pub enum ClientEvent {
ListGames(Vec<Game>),
GotGameFiles {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
DownloadGameFilesBegin {
id: String,
},
DownloadGameFilesFinished {
id: String,
},
DownloadGameFilesFailed {
id: String,
},
}
#[derive(Debug)]
pub enum ClientCommand {
ListGames,
GetGame(u64),
GetGame(String),
DownloadGameFiles {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
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(
mut rx_control: UnboundedReceiver<ClientCommand>,
tx_event: UnboundedSender<ClientEvent>,
tx_notify_ui: UnboundedSender<ClientEvent>,
) -> eyre::Result<()> {
// blocking wait for remote address
log::debug!("waiting for server address");
@ -34,10 +179,15 @@ pub async fn run(
}
};
// client context
let ctx = Ctx {
game_dir: Arc::new(RwLock::new(None)),
};
loop {
let limits = Limits::default()
.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()
.with_tls(CERT_PEM)?
@ -45,8 +195,8 @@ pub async fn run(
.with_limits(limits)?
.start()?;
let conn = Connect::new(server_addr).with_server_name("localhost");
let mut conn = match client.connect(conn).await {
let connection = Connect::new(server_addr).with_server_name("localhost");
let mut conn = match client.connect(connection.clone()).await {
Ok(conn) => conn,
Err(e) => {
log::error!("failed to connect to server: {e}");
@ -57,6 +207,10 @@ pub async fn run(
conn.keep_alive(true)?;
if !initial_server_alive_check(&mut conn).await {
continue;
}
log::info!(
"connected: (server: {}) (client: {})",
maybe_addr!(conn.remote_addr()),
@ -67,15 +221,85 @@ pub async fn run(
while let Some(cmd) = rx_control.recv().await {
let request = match cmd {
ClientCommand::ListGames => Request::ListGames,
ClientCommand::GetGame(id) => Request::GetGame { id },
ClientCommand::ServerAddr(_) => Request::Invalid(
[].into(),
"invalid control message (ServerAddr), should not happen".into(),
),
ClientCommand::GetGame(id) => {
log::info!("requesting game from server: {id}");
Request::GetGame { id }
}
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();
log::error!("encoded data: {}", String::from_utf8_lossy(&data));
log::trace!("encoded data: {}", String::from_utf8_lossy(&data));
let stream = match conn.open_bidirectional_stream().await {
Ok(stream) => stream,
@ -88,28 +312,69 @@ pub async fn run(
let (mut rx, mut tx) = stream.split();
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:?}");
}
if let Ok(Some(data)) = rx.receive().await {
log::trace!("server response (raw): {}", String::from_utf8_lossy(&data));
let mut data = BytesMut::new();
while let Ok(Some(bytes)) = rx.receive().await {
data.extend_from_slice(&bytes);
}
log::debug!("{} bytes received from server", data.len());
log::trace!("msg: (raw): {}", String::from_utf8_lossy(&data));
let response = Response::decode(data.freeze());
log::trace!("msg: {response:?}");
let response = Response::decode(&data);
log::trace!(
"server response (decoded): {}",
String::from_utf8_lossy(&data)
);
match response {
Response::Games(games) => {
Response::ListGames(games) => {
for game in &games {
log::debug!("{game:?}");
log::trace!("{game}");
}
if let Err(e) = tx_event.send(ClientEvent::ListGames(games)) {
if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) {
log::error!("failed to send ClientEvent::ListGames to client {e:?}");
}
}
Response::Game(game) => log::debug!("game received: {game:?}"),
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::GameNotFound(id) => log::debug!("game not found {id}"),
Response::InvalidRequest(request_bytes, err) => log::error!(
"server says our request was invalid (error: {}): {}",
@ -126,6 +391,7 @@ pub async fn run(
String::from_utf8_lossy(&data)
);
}
Response::Pong => (), // ignore (should never happen)
}
if let Err(err) = tx.close().await {
@ -134,61 +400,3 @@ pub async fn run(
}
}
}
// log::info!("server closed connection");
// Ok(())
}
// #[derive(Debug, Parser)]
// struct Cli {
// /// Server IP address.
// #[clap(long, default_value = "127.0.0.1")]
// ip: IpAddr,
// /// Server port.
// #[clap(long, default_value = "13337")]
// port: u16,
// }
// #[tokio::main]
// async fn main() -> eyre::Result<()> {
// let cli = Cli::parse();
// let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel::<ControlMessage>();
// // Spawn client in a separate task
// let client_handle = tokio::spawn(async move {
// let remote_addr = SocketAddr::from((cli.ip, cli.port));
// Client::run(remote_addr, rx_control).await
// });
// // Handle stdin commands in the main task
// let mut stdin = BufReader::new(tokio::io::stdin());
// let mut line = String::new();
// loop {
// line.clear();
// if stdin.read_line(&mut line).await? == 0 {
// break; // EOF reached
// }
// // Trim whitespace and handle commands
// match line.trim() {
// "list" => {
// tx_control.send(ControlMessage::ListGames)?;
// }
// cmd if cmd.starts_with("get ") => {
// if let Ok(id) = cmd[4..].trim().parse::<u64>() {
// tx_control.send(ControlMessage::GetGame(id))?;
// } else {
// println!("Invalid game ID");
// }
// }
// "quit" | "exit" => break,
// "" => continue,
// _ => println!("Unknown command. Available commands: list, get <id>, quit"),
// }
// }
// client_handle.await??;
// Ok(())
// }

View File

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

View File

@ -0,0 +1,66 @@
use std::path::Path;
use lanspread_db::db::Game;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePool;
#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct EtiGame {
pub game_id: String,
pub game_title: String,
pub game_key: String,
pub game_release: String,
pub game_publisher: String,
pub game_size: f64,
pub game_readme_de: String,
pub game_readme_en: String,
pub game_readme_fr: String,
pub game_maxplayers: u32,
pub game_master_req: i32,
pub genre_de: String,
pub game_version: String,
}
/// # Errors
pub async fn get_games(db: &Path) -> eyre::Result<Vec<EtiGame>> {
let pool = SqlitePool::connect(format!("sqlite:{}", db.to_string_lossy()).as_str()).await?;
let mut games = sqlx::query_as::<_, EtiGame>(
"SELECT
g.game_id, g.game_title, g.game_key, g.game_release,
g.game_publisher, CAST(g.game_size AS REAL) as game_size, g.game_readme_de,
g.game_readme_en, g.game_readme_fr, CAST(g.game_maxplayers AS INTEGER) as game_maxplayers,
g.game_master_req, ge.genre_de, g.game_version
FROM games g
JOIN genre ge ON g.genre_id = ge.genre_id",
)
.fetch_all(&pool)
.await?;
games.sort_by(|a, b| a.game_title.cmp(&b.game_title));
tracing::info!("Found {} games in game.db", games.len());
for game in &games {
tracing::debug!("{}: {}", game.game_id, game.game_title);
}
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

@ -0,0 +1 @@
pub mod eti;

View File

@ -1,7 +1,7 @@
[package]
name = "lanspread-db"
version = "0.1.0"
edition = "2021"
edition = "2024"
[lints.rust]
unsafe_code = "forbid"
@ -13,7 +13,9 @@ unwrap_used = "warn"
[dependencies]
# external
bytes = { workspace = true }
eyre = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }

View File

@ -1,48 +1,46 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::doc_markdown)]
use std::{
collections::HashMap,
fmt,
fs::{File, OpenOptions},
path::Path,
};
use std::{collections::HashMap, fmt, path::Path};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use crate::serialization::version_serde;
/// A game
#[derive(Clone, Serialize, Deserialize)]
pub struct Game {
/// example: 1
pub id: u64,
/// example: Call of Duty 3
/// example: aoe2
pub id: String,
/// example: Age of Empires 2
pub name: String,
/// example: A shooter game in war.
/// example: Dieses Paket enthält die original AoE 2 Version,...
pub description: String,
/// example: `call_of_duty.tar.zst`
pub install_archive: String,
/// example: 1999
pub release_year: String,
/// Microsoft
pub publisher: String,
/// example: 8
pub max_players: u32,
/// example: 1.0.0
#[serde(with = "version_serde")]
pub version: semver::Version,
/// size (bytes)
/// example: 3.5
pub version: String,
/// example: Echtzeit-Strategie
pub genre: String,
/// size in bytes: example: 3455063152
pub size: u64,
/// thumbnail image
pub thumbnail: Option<Bytes>,
/// only relevant for client (yeah... I know)
pub installed: bool,
}
impl fmt::Debug for Game {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {} {} ({} players) ({}: {} MB) {}",
"{}: {} ({} MB)",
self.id,
self.name,
self.version,
self.max_players,
self.install_archive,
self.size,
self.description,
self.size / 1024 / 1024,
)
}
}
@ -75,56 +73,51 @@ impl Ord for Game {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameDB {
pub games: HashMap<u64, Game>,
next_id: u64,
pub games: HashMap<String, Game>,
}
impl GameDB {
#[must_use]
pub fn new() -> Self {
pub fn empty() -> Self {
GameDB {
games: HashMap::new(),
next_id: 1,
}
}
#[must_use]
pub fn from(games: Vec<Game>) -> Self {
let mut db = GameDB::new();
let mut db = GameDB::empty();
for game in games {
let id = game.id;
db.games.insert(game.id, game);
db.next_id = db.next_id.max(id + 1);
db.games.insert(game.id.clone(), game);
}
db
}
pub fn add_game<S: Into<String>>(
&mut self,
name: S,
description: S,
install_archive: S,
max_players: u32,
version: semver::Version,
) -> u64 {
let id = self.next_id;
self.next_id += 1;
let game = Game {
id,
name: name.into(),
description: description.into(),
install_archive: install_archive.into(),
max_players,
version,
size: 0,
};
self.games.insert(id, game);
id
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]
pub fn get_game_by_id(&self, id: u64) -> Option<&Game> {
self.games.get(&id)
pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
where
S: AsRef<str>,
{
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]
@ -132,60 +125,49 @@ impl GameDB {
self.games.values().find(|game| game.name == name)
}
pub fn update_game<S: Into<String>>(
&mut self,
id: u64,
name: Option<S>,
description: Option<S>,
install_archive: Option<S>,
) -> bool {
if let Some(game) = self.games.get_mut(&id) {
if let Some(new_name) = name {
game.name = new_name.into();
}
if let Some(new_description) = description {
game.description = new_description.into();
}
if let Some(archive) = install_archive {
game.install_archive = archive.into();
}
true
} else {
false
}
}
pub fn delete_game(&mut self, id: u64) -> bool {
self.games.remove(&id).is_some()
}
#[must_use]
pub fn all_games(&self) -> Vec<&Game> {
self.games.values().collect()
let mut games: Vec<_> = self.games.values().collect();
games.sort_by(|a, b| a.name.cmp(&b.name));
games
}
pub fn save_to_file(&self, path: &Path) -> eyre::Result<()> {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
let games: Vec<&Game> = self.games.values().collect();
serde_json::to_writer(file, &games)?;
Ok(())
pub fn set_all_uninstalled(&mut self) {
for game in self.games.values_mut() {
game.installed = false;
}
pub fn load_from_file(path: &Path) -> eyre::Result<Self> {
let file = File::open(path)?;
let games: Vec<Game> = serde_json::from_reader(file)?;
let db = GameDB::from(games);
Ok(db)
}
}
impl Default for GameDB {
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,2 +1 @@
pub mod db;
mod serialization;

View File

@ -1,19 +0,0 @@
pub(crate) mod version_serde {
use semver::Version;
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&version.to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Version::parse(&s).map_err(serde::de::Error::custom)
}
}

View File

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

View File

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

View File

@ -1,43 +1,44 @@
use bytes::Bytes;
use lanspread_db::db::Game;
use lanspread_db::db::{Game, GameFileDescription};
use serde::{Deserialize, Serialize};
use tracing::error;
#[derive(Debug, Serialize, Deserialize)]
pub enum Request {
Ping,
ListGames,
GetGame { id: u64 },
Invalid(Vec<u8>, String),
GetGame { id: String },
GetGameFileData(GameFileDescription),
Invalid(Bytes, String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Response {
Games(Vec<Game>),
Game(Game),
GameNotFound(u64),
InvalidRequest(Vec<u8>, String),
Pong,
ListGames(Vec<Game>),
GetGame {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
GameNotFound(String),
InvalidRequest(Bytes, String),
EncodingError(String),
DecodingError(Vec<u8>, String),
DecodingError(Bytes, String),
}
// Add Message trait
pub trait Message {
fn decode(bytes: &[u8]) -> Self;
fn decode(bytes: Bytes) -> Self;
fn encode(&self) -> Bytes;
}
// Implement for Request
impl Message for Request {
fn decode(bytes: &[u8]) -> Self {
match serde_json::from_slice(bytes) {
fn decode(bytes: Bytes) -> Self {
match serde_json::from_slice(&bytes) {
Ok(t) => t,
Err(e) => {
tracing::error!(
"got invalid request from client (error: {}): {}",
e,
String::from_utf8_lossy(bytes)
);
Request::Invalid(bytes.into(), e.to_string())
tracing::error!(?e, "Request decoding error");
Request::Invalid(bytes, e.to_string())
}
}
}
@ -46,7 +47,7 @@ impl Message for Request {
match serde_json::to_vec(self) {
Ok(s) => Bytes::from(s),
Err(e) => {
error!(?e, "Request encoding error");
tracing::error!(?e, "Request encoding error");
Bytes::from(format!(r#"{{"error": "encoding error: {e}"}}"#))
}
}
@ -55,10 +56,13 @@ impl Message for Request {
// Implement for Response
impl Message for Response {
fn decode(bytes: &[u8]) -> Self {
match serde_json::from_slice(bytes) {
fn decode(bytes: Bytes) -> Self {
match serde_json::from_slice(&bytes) {
Ok(t) => t,
Err(e) => Response::DecodingError(bytes.into(), 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) {
Ok(s) => Bytes::from(s),
Err(e) => {
error!(?e, "Response encoding error");
tracing::error!(?e, "Response encoding error");
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]
name = "lanspread-server"
version = "0.1.0"
edition = "2021"
edition = "2024"
[lints.rust]
unsafe_code = "forbid"
@ -13,6 +13,7 @@ unwrap_used = "warn"
[dependencies]
# local
lanspread-compat = { path = "../lanspread-compat" }
lanspread-db = { path = "../lanspread-db" }
lanspread-mdns = { path = "../lanspread-mdns" }
lanspread-proto = { path = "../lanspread-proto" }
@ -20,8 +21,10 @@ lanspread-utils = { path = "../lanspread-utils" }
# external
bytes = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
eyre = { workspace = true }
gethostname = { workspace = true }
itertools = { workspace = true }
s2n-quic = { workspace = true }
serde_json = { workspace = true }
@ -29,3 +32,5 @@ semver = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
uuid = { workspace = true }
walkdir = { workspace = true }

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,173 +1,87 @@
use std::{
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
sync::Arc,
};
mod cli;
mod quic;
mod req;
use clap::Parser;
use lanspread_db::db::GameDB;
use std::{convert::Into, net::SocketAddr, time::Duration};
use chrono::{DateTime, Local};
use clap::Parser as _;
use cli::Cli;
use gethostname::gethostname;
use lanspread_compat::eti;
use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{
DaemonEvent,
MdnsAdvertiser,
LANSPREAD_INSTANCE_NAME,
LANSPREAD_SERVICE_TYPE,
DaemonEvent, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser,
};
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::Server as QuicServer;
use testing::generate_test_db;
use tokio::{io::AsyncWriteExt, sync::Mutex};
use tracing_subscriber::EnvFilter;
use uuid::Uuid;
mod testing;
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"));
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
let uidddd = Uuid::now_v7();
pub(crate) struct Server {
db_path: PathBuf,
// TODO
let uidddd = uidddd
.get_timestamp()
.expect("failed to get timestamp from UUID")
.to_unix();
let local_datetime: DateTime<Local> =
DateTime::from_timestamp(i64::try_from(uidddd.0).unwrap_or(0), uidddd.1)
.expect("Failed to create DateTime from uuid unix timestamp")
.into();
dbg!(local_datetime);
let hostname = gethostname();
let mut hostname = hostname.to_str().unwrap_or("");
if hostname.len() + peer_id.len() > 63 {
hostname = &hostname[..63 - peer_id.len()];
}
format!("{hostname}-{peer_id}")
} else {
String::from(LANSPREAD_INSTANCE_NAME)
};
#[derive(Clone, Debug)]
struct ServerCtx {
handler: RequestHandler,
}
let mdns = MdnsAdvertiser::new(LANSPREAD_SERVICE_TYPE, &combined_str, server_addr)?;
#[derive(Clone, Debug)]
struct ConnectionCtx {
server_ctx: Arc<ServerCtx>,
remote_addr: String,
}
impl Server {
fn new<S: Into<PathBuf>>(db_path: S) -> Self {
Server {
db_path: db_path.into(),
}
}
async fn run(&mut self, addr: SocketAddr) -> eyre::Result<()> {
let mut server = QuicServer::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io(addr)?
.start()?;
let server_ctx = Arc::new(ServerCtx {
handler: RequestHandler::new(&self.db_path)?,
});
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 {
tracing::info!("{} connected", conn_ctx.remote_addr);
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
let (mut rx, mut tx) = stream.split();
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);
while let Ok(event) = mdns.monitor.recv() {
tracing::trace!("mDNS: {:?}", &event);
if let DaemonEvent::Error(e) = event {
tracing::error!("mDNS: {e}");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
});
}
});
}
Ok(())
}
}
#[derive(Clone, Debug)]
struct RequestHandler {
db: Arc<Mutex<GameDB>>,
}
async fn prepare_game_db(cli: &Cli) -> eyre::Result<GameDB> {
// build games from ETI database
let mut games: Vec<Game> = eti::get_games(&cli.db)
.await?
.into_iter()
.map(Into::into)
.collect();
impl RequestHandler {
fn new(db_path: &Path) -> eyre::Result<Self> {
let db = GameDB::load_from_file(db_path)?;
Ok(RequestHandler {
db: Arc::new(Mutex::new(db)),
})
}
// filter out games that the server does not have in game_dir
games.retain(|game| cli.game_dir.join(&game.id).is_dir());
async fn handle_request(&self, request: Request) -> Response {
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)
}
}
}
}
let mut game_db = GameDB::from(games);
const GAME_DB_PATH: &str = "/home/pfs/shm/game.db";
game_db.add_thumbnails(&cli.thumbs_dir);
#[derive(Debug, Parser)]
struct Cli {
/// The IP address to bind to.
#[clap(long, default_value = "127.0.0.1")]
ip: IpAddr,
/// The listen port.
#[clap(long, default_value = "13337")]
port: u16,
game_db.all_games().iter().for_each(|game| {
tracing::debug!("Found game: {game}");
});
tracing::info!("Prepared game database with {} games", game_db.games.len());
Ok(game_db)
}
#[tokio::main]
@ -178,26 +92,19 @@ async fn main() -> eyre::Result<()> {
let cli = Cli::parse();
generate_test_db(GAME_DB_PATH);
assert!(
cli.game_dir.exists(),
"Games folder does not exist: {}",
cli.game_dir.to_str().expect("Invalid path")
);
let mdns = MdnsAdvertiser::new(
LANSPREAD_SERVICE_TYPE,
LANSPREAD_INSTANCE_NAME,
(cli.ip, cli.port).into(),
)?;
let server_addr = SocketAddr::from((cli.ip, cli.port));
tokio::spawn(async move {
while let Ok(event) = mdns.monitor.recv() {
tracing::info!("mDNS: {:?}", &event);
if let DaemonEvent::Error(e) = event {
tracing::info!("Failed: {e}");
break;
}
}
});
tracing::info!("Server listening on {}:{}", cli.ip, cli.port);
let mut server = Server::new(GAME_DB_PATH);
server.run(SocketAddr::from((cli.ip, cli.port))).await
// spawn mDNS listener task
spawn_mdns_task(server_addr)?;
let game_db = prepare_game_db(&cli).await?;
tracing::info!("Server listening on {server_addr}");
crate::quic::run_server(server_addr, game_db, cli.game_dir).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,35 +0,0 @@
#![allow(clippy::unwrap_used)]
use std::path::PathBuf;
use lanspread_db::db::GameDB;
pub(crate) fn generate_test_db<P: Into<PathBuf>>(db_path: P) {
let db_path = db_path.into();
let mut db = GameDB::new();
db.add_game(
"Call of Duty 3",
"A shooter game in war.",
"call_of_duty.tar.zst",
64,
semver::Version::new(1, 0, 0),
);
db.add_game(
"Counter-Strike Source",
"Valve's iconic shooter.",
"cstrike.tar.zst",
32,
semver::Version::new(1, 0, 0),
);
db.add_game(
"Factorio",
"Best game of all time, seriously.",
"factorio.tar.zst",
128,
semver::Version::new(1, 0, 0),
);
db.update_game(1, Some("Call of Duty 4"), None, None);
db.save_to_file(&db_path).unwrap();
}

View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,7 @@
# Tauri + React + Typescript
This template should help get you started developing with Tauri, React and Typescript in Vite.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

636
crates/lanspread-tauri-deno-ts/deno.lock generated Normal file
View File

@ -0,0 +1,636 @@
{
"version": "4",
"specifiers": {
"npm:@tauri-apps/api@^2.3.0": "2.3.0",
"npm:@tauri-apps/cli@^2.3.1": "2.3.1",
"npm:@tauri-apps/plugin-dialog@^2.2.0": "2.2.0",
"npm:@tauri-apps/plugin-shell@^2.2.0": "2.2.0",
"npm:@tauri-apps/plugin-store@^2.2.0": "2.2.0",
"npm:@types/react-dom@^19.0.4": "19.0.4_@types+react@19.0.10",
"npm:@types/react@^19.0.10": "19.0.10",
"npm:@types/react@^19.0.12": "19.0.12",
"npm:@vitejs/plugin-react@^4.3.4": "4.3.4_vite@6.2.2_@babel+core@7.26.10",
"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": {
"@ampproject/remapping@2.3.0": {
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dependencies": [
"@jridgewell/gen-mapping",
"@jridgewell/trace-mapping"
]
},
"@babel/code-frame@7.26.2": {
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dependencies": [
"@babel/helper-validator-identifier",
"js-tokens",
"picocolors"
]
},
"@babel/compat-data@7.26.8": {
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="
},
"@babel/core@7.26.10": {
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dependencies": [
"@ampproject/remapping",
"@babel/code-frame",
"@babel/generator",
"@babel/helper-compilation-targets",
"@babel/helper-module-transforms",
"@babel/helpers",
"@babel/parser",
"@babel/template",
"@babel/traverse",
"@babel/types",
"convert-source-map",
"debug",
"gensync",
"json5",
"semver"
]
},
"@babel/generator@7.26.10": {
"integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
"dependencies": [
"@babel/parser",
"@babel/types",
"@jridgewell/gen-mapping",
"@jridgewell/trace-mapping",
"jsesc"
]
},
"@babel/helper-compilation-targets@7.26.5": {
"integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
"dependencies": [
"@babel/compat-data",
"@babel/helper-validator-option",
"browserslist",
"lru-cache",
"semver"
]
},
"@babel/helper-module-imports@7.25.9": {
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
"dependencies": [
"@babel/traverse",
"@babel/types"
]
},
"@babel/helper-module-transforms@7.26.0_@babel+core@7.26.10": {
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
"dependencies": [
"@babel/core",
"@babel/helper-module-imports",
"@babel/helper-validator-identifier",
"@babel/traverse"
]
},
"@babel/helper-plugin-utils@7.26.5": {
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="
},
"@babel/helper-string-parser@7.25.9": {
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="
},
"@babel/helper-validator-identifier@7.25.9": {
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="
},
"@babel/helper-validator-option@7.25.9": {
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="
},
"@babel/helpers@7.26.10": {
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"dependencies": [
"@babel/template",
"@babel/types"
]
},
"@babel/parser@7.26.10": {
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"dependencies": [
"@babel/types"
]
},
"@babel/plugin-transform-react-jsx-self@7.25.9_@babel+core@7.26.10": {
"integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
"dependencies": [
"@babel/core",
"@babel/helper-plugin-utils"
]
},
"@babel/plugin-transform-react-jsx-source@7.25.9_@babel+core@7.26.10": {
"integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
"dependencies": [
"@babel/core",
"@babel/helper-plugin-utils"
]
},
"@babel/template@7.26.9": {
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"dependencies": [
"@babel/code-frame",
"@babel/parser",
"@babel/types"
]
},
"@babel/traverse@7.26.10": {
"integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==",
"dependencies": [
"@babel/code-frame",
"@babel/generator",
"@babel/parser",
"@babel/template",
"@babel/types",
"debug",
"globals"
]
},
"@babel/types@7.26.10": {
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"dependencies": [
"@babel/helper-string-parser",
"@babel/helper-validator-identifier"
]
},
"@esbuild/aix-ppc64@0.25.1": {
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="
},
"@esbuild/android-arm64@0.25.1": {
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="
},
"@esbuild/android-arm@0.25.1": {
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="
},
"@esbuild/android-x64@0.25.1": {
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="
},
"@esbuild/darwin-arm64@0.25.1": {
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="
},
"@esbuild/darwin-x64@0.25.1": {
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="
},
"@esbuild/freebsd-arm64@0.25.1": {
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="
},
"@esbuild/freebsd-x64@0.25.1": {
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="
},
"@esbuild/linux-arm64@0.25.1": {
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="
},
"@esbuild/linux-arm@0.25.1": {
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="
},
"@esbuild/linux-ia32@0.25.1": {
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="
},
"@esbuild/linux-loong64@0.25.1": {
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="
},
"@esbuild/linux-mips64el@0.25.1": {
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="
},
"@esbuild/linux-ppc64@0.25.1": {
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="
},
"@esbuild/linux-riscv64@0.25.1": {
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="
},
"@esbuild/linux-s390x@0.25.1": {
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="
},
"@esbuild/linux-x64@0.25.1": {
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="
},
"@esbuild/netbsd-arm64@0.25.1": {
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="
},
"@esbuild/netbsd-x64@0.25.1": {
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="
},
"@esbuild/openbsd-arm64@0.25.1": {
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="
},
"@esbuild/openbsd-x64@0.25.1": {
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="
},
"@esbuild/sunos-x64@0.25.1": {
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="
},
"@esbuild/win32-arm64@0.25.1": {
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="
},
"@esbuild/win32-ia32@0.25.1": {
"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": [
"@jridgewell/set-array",
"@jridgewell/sourcemap-codec",
"@jridgewell/trace-mapping"
]
},
"@jridgewell/resolve-uri@3.1.2": {
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
},
"@jridgewell/set-array@1.2.1": {
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
},
"@jridgewell/sourcemap-codec@1.5.0": {
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"@jridgewell/trace-mapping@0.3.25": {
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": [
"@jridgewell/resolve-uri",
"@jridgewell/sourcemap-codec"
]
},
"@rollup/rollup-android-arm-eabi@4.36.0": {
"integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w=="
},
"@rollup/rollup-android-arm64@4.36.0": {
"integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg=="
},
"@rollup/rollup-darwin-arm64@4.36.0": {
"integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw=="
},
"@rollup/rollup-darwin-x64@4.36.0": {
"integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA=="
},
"@rollup/rollup-freebsd-arm64@4.36.0": {
"integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg=="
},
"@rollup/rollup-freebsd-x64@4.36.0": {
"integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ=="
},
"@rollup/rollup-linux-arm-gnueabihf@4.36.0": {
"integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg=="
},
"@rollup/rollup-linux-arm-musleabihf@4.36.0": {
"integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg=="
},
"@rollup/rollup-linux-arm64-gnu@4.36.0": {
"integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A=="
},
"@rollup/rollup-linux-arm64-musl@4.36.0": {
"integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw=="
},
"@rollup/rollup-linux-loongarch64-gnu@4.36.0": {
"integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg=="
},
"@rollup/rollup-linux-powerpc64le-gnu@4.36.0": {
"integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg=="
},
"@rollup/rollup-linux-riscv64-gnu@4.36.0": {
"integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA=="
},
"@rollup/rollup-linux-s390x-gnu@4.36.0": {
"integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag=="
},
"@rollup/rollup-linux-x64-gnu@4.36.0": {
"integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ=="
},
"@rollup/rollup-linux-x64-musl@4.36.0": {
"integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ=="
},
"@rollup/rollup-win32-arm64-msvc@4.36.0": {
"integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A=="
},
"@rollup/rollup-win32-ia32-msvc@4.36.0": {
"integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ=="
},
"@rollup/rollup-win32-x64-msvc@4.36.0": {
"integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="
},
"@tauri-apps/api@2.3.0": {
"integrity": "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="
},
"@tauri-apps/cli-darwin-arm64@2.3.1": {
"integrity": "sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw=="
},
"@tauri-apps/cli-darwin-x64@2.3.1": {
"integrity": "sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA=="
},
"@tauri-apps/cli-linux-arm-gnueabihf@2.3.1": {
"integrity": "sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA=="
},
"@tauri-apps/cli-linux-arm64-gnu@2.3.1": {
"integrity": "sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg=="
},
"@tauri-apps/cli-linux-arm64-musl@2.3.1": {
"integrity": "sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w=="
},
"@tauri-apps/cli-linux-x64-gnu@2.3.1": {
"integrity": "sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA=="
},
"@tauri-apps/cli-linux-x64-musl@2.3.1": {
"integrity": "sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA=="
},
"@tauri-apps/cli-win32-arm64-msvc@2.3.1": {
"integrity": "sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww=="
},
"@tauri-apps/cli-win32-ia32-msvc@2.3.1": {
"integrity": "sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw=="
},
"@tauri-apps/cli-win32-x64-msvc@2.3.1": {
"integrity": "sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q=="
},
"@tauri-apps/cli@2.3.1": {
"integrity": "sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==",
"dependencies": [
"@tauri-apps/cli-darwin-arm64",
"@tauri-apps/cli-darwin-x64",
"@tauri-apps/cli-linux-arm-gnueabihf",
"@tauri-apps/cli-linux-arm64-gnu",
"@tauri-apps/cli-linux-arm64-musl",
"@tauri-apps/cli-linux-x64-gnu",
"@tauri-apps/cli-linux-x64-musl",
"@tauri-apps/cli-win32-arm64-msvc",
"@tauri-apps/cli-win32-ia32-msvc",
"@tauri-apps/cli-win32-x64-msvc"
]
},
"@tauri-apps/plugin-dialog@2.2.0": {
"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": [
"@tauri-apps/api"
]
},
"@types/babel__core@7.20.5": {
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dependencies": [
"@babel/parser",
"@babel/types",
"@types/babel__generator",
"@types/babel__template",
"@types/babel__traverse"
]
},
"@types/babel__generator@7.6.8": {
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
"dependencies": [
"@babel/types"
]
},
"@types/babel__template@7.4.4": {
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dependencies": [
"@babel/parser",
"@babel/types"
]
},
"@types/babel__traverse@7.20.6": {
"integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
"dependencies": [
"@babel/types"
]
},
"@types/estree@1.0.6": {
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"@types/react-dom@19.0.4_@types+react@19.0.10": {
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"dependencies": [
"@types/react@19.0.10"
]
},
"@types/react@19.0.10": {
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dependencies": [
"csstype"
]
},
"@types/react@19.0.12": {
"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": [
"@babel/core",
"@babel/plugin-transform-react-jsx-self",
"@babel/plugin-transform-react-jsx-source",
"@types/babel__core",
"react-refresh",
"vite"
]
},
"browserslist@4.24.4": {
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"dependencies": [
"caniuse-lite",
"electron-to-chromium",
"node-releases",
"update-browserslist-db"
]
},
"caniuse-lite@1.0.30001706": {
"integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug=="
},
"convert-source-map@2.0.0": {
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"debug@4.4.0": {
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": [
"ms"
]
},
"electron-to-chromium@1.5.122": {
"integrity": "sha512-EML1wnwkY5MFh/xUnCvY8FrhUuKzdYhowuZExZOfwJo+Zu9OsNCI23Cgl5y7awy7HrUHSwB1Z8pZX5TI34lsUg=="
},
"esbuild@0.25.1": {
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
"@esbuild/android-x64",
"@esbuild/darwin-arm64",
"@esbuild/darwin-x64",
"@esbuild/freebsd-arm64",
"@esbuild/freebsd-x64",
"@esbuild/linux-arm",
"@esbuild/linux-arm64",
"@esbuild/linux-ia32",
"@esbuild/linux-loong64",
"@esbuild/linux-mips64el",
"@esbuild/linux-ppc64",
"@esbuild/linux-riscv64",
"@esbuild/linux-s390x",
"@esbuild/linux-x64",
"@esbuild/netbsd-arm64",
"@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64",
"@esbuild/sunos-x64",
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
]
},
"escalade@3.2.0": {
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
},
"fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
},
"gensync@1.0.0-beta.2": {
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
},
"globals@11.12.0": {
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"js-tokens@4.0.0": {
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"jsesc@3.1.0": {
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="
},
"json5@2.2.3": {
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"lru-cache@5.1.1": {
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dependencies": [
"yallist"
]
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"nanoid@3.3.11": {
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
},
"node-releases@2.0.19": {
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"postcss@8.5.3": {
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dependencies": [
"nanoid",
"picocolors",
"source-map-js"
]
},
"react-dom@19.0.0_react@19.0.0": {
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"dependencies": [
"react",
"scheduler"
]
},
"react-refresh@0.14.2": {
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="
},
"react@19.0.0": {
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="
},
"rollup@4.36.0": {
"integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==",
"dependencies": [
"@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64",
"@rollup/rollup-darwin-x64",
"@rollup/rollup-freebsd-arm64",
"@rollup/rollup-freebsd-x64",
"@rollup/rollup-linux-arm-gnueabihf",
"@rollup/rollup-linux-arm-musleabihf",
"@rollup/rollup-linux-arm64-gnu",
"@rollup/rollup-linux-arm64-musl",
"@rollup/rollup-linux-loongarch64-gnu",
"@rollup/rollup-linux-powerpc64le-gnu",
"@rollup/rollup-linux-riscv64-gnu",
"@rollup/rollup-linux-s390x-gnu",
"@rollup/rollup-linux-x64-gnu",
"@rollup/rollup-linux-x64-musl",
"@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-msvc",
"@types/estree",
"fsevents"
]
},
"scheduler@0.25.0": {
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
},
"semver@6.3.1": {
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
},
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"typescript@5.8.2": {
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="
},
"update-browserslist-db@1.1.3_browserslist@4.24.4": {
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dependencies": [
"browserslist",
"escalade",
"picocolors"
]
},
"vite@6.2.2": {
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"dependencies": [
"esbuild",
"fsevents",
"postcss",
"rollup"
]
},
"yallist@3.1.1": {
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
},
"workspace": {
"packageJson": {
"dependencies": [
"npm:@tauri-apps/api@^2.3.0",
"npm:@tauri-apps/cli@^2.3.1",
"npm:@tauri-apps/plugin-dialog@^2.2.0",
"npm:@tauri-apps/plugin-shell@^2.2.0",
"npm:@tauri-apps/plugin-store@^2.2.0",
"npm:@types/react-dom@^19.0.4",
"npm:@types/react@^19.0.12",
"npm:@vitejs/plugin-react@^4.3.4",
"npm:react-dom@19",
"npm:react@19",
"npm:typescript@^5.8.2",
"npm:vite@^6.2.2"
]
}
}
}

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,28 @@
{
"name": "lanspread-tauri-deno-ts",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-store": "^2.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.2.0"
},
"devDependencies": {
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.8.2",
"vite": "^6.2.2",
"@tauri-apps/cli": "^2.3.1"
}
}

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[package]
name = "lanspread-tauri-leptos"
name = "lanspread-tauri-deno-ts"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -11,7 +11,7 @@ edition = "2021"
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "lanspread_tauri_leptos_lib"
name = "lanspread_tauri_deno_ts_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
@ -25,10 +25,15 @@ lanspread-mdns = { path = "../../lanspread-mdns" }
# external
eyre = { workspace = true }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = [] }
tauri-plugin-log = "2"
tauri-plugin-shell = "2"
tokio = { version = "1.41", features = ["full"] }
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
tauri-plugin-log = { workspace = true }
tauri-plugin-shell = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-store = { workspace = true }
tokio = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,485 @@
use std::{
collections::HashSet,
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
use eyre::bail;
use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service};
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/
struct LanSpreadState {
server_addr: RwLock<Option<SocketAddr>>,
client_ctrl: UnboundedSender<ClientCommand>,
games: Arc<RwLock<GameDB>>,
games_in_download: Arc<Mutex<HashSet<String>>>,
games_folder: Arc<RwLock<String>>,
}
#[tauri::command]
fn request_games(state: tauri::State<LanSpreadState>) {
log::debug!("request_games");
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::ListGames) {
log::error!("Failed to send message to client: {e:?}");
}
}
#[tauri::command]
fn install_game(id: String, state: tauri::State<LanSpreadState>) -> bool {
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
});
if already_in_download {
return false;
}
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) {
log::error!("Failed to send message to client: {e:?}");
}
true
}
#[cfg(target_os = "windows")]
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
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) {
log::info!("Looking for server...");
loop {
match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) {
Ok(server_addr) => {
log::info!("Found server at {server_addr}");
let state: tauri::State<LanSpreadState> = app.state();
*state.server_addr.write().await = Some(server_addr);
state
.client_ctrl
.send(ClientCommand::ServerAddr(server_addr))
.unwrap();
request_games(state);
break;
}
Err(e) => {
log::warn!("Failed to find server: {e} - retrying...");
}
}
}
}
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)]
pub fn run() {
let tauri_logger_builder = tauri_plugin_log::Builder::new()
.clear_targets()
.target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::Stdout,
))
.level(log::LevelFilter::Info)
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
// channel to pass commands to the client
let (tx_client_control, rx_client_control) =
tokio::sync::mpsc::unbounded_channel::<ClientCommand>();
// channel to receive events from the client
let (tx_client_event, mut rx_client_event) =
tokio::sync::mpsc::unbounded_channel::<ClientEvent>();
let lanspread_state = LanSpreadState {
server_addr: RwLock::new(None),
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()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_logger_builder.build())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
request_games,
install_game,
run_game,
update_game_directory
])
.manage(lanspread_state)
.setup(|app| {
let app_handle = app.handle().clone();
// discover server
tauri::async_runtime::spawn(async move { find_server(app_handle).await });
tauri::async_runtime::spawn(async move {
lanspread_client::run(rx_client_control, tx_client_event).await
});
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
while let Some(event) = rx_client_event.recv().await {
match event {
ClientEvent::ListGames(games) => {
log::info!("ClientEvent::ListGames received");
update_game_db(games, app_handle.clone()).await;
}
ClientEvent::GotGameFiles { id, file_descriptions } => {
log::info!("ClientEvent::GotGameFiles received");
if let Err(e) = app_handle.emit(
"game-download-pre",
Some(id.clone()),
) {
log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}");
}
app_handle
.state::<LanSpreadState>()
.inner()
.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());
},
}
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
lanspread_tauri_leptos_lib::run()
lanspread_tauri_deno_ts_lib::run()
}

View File

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

View File

@ -0,0 +1,186 @@
body {
background-color: #000313;
font-family: Arial, sans-serif;
color: #D5DBFE;
margin: 0;
padding: 0;
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #000313;
z-index: 1000;
padding-top: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
h1.align-center {
margin: 0;
padding: 10px 0;
}
.main-header {
width: 100%;
}
.grid-container {
margin-top: 160px; /* Adjust based on your header height */
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.item {
display: flex;
flex-direction: column;
background: linear-gradient(to bottom, black, #000938);
color: white;
border: 1px solid #444;
border-radius: 8px;
overflow: hidden;
transition: background 0.3s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
cursor: pointer;
/* max-width: 280px; */
}
.item:hover {
background: linear-gradient(to bottom, black, #3849AB);
}
.item img {
width: 280px; /* Fixed width */
height: 200px; /* Fixed height */
object-fit: cover;
display: block; /* Removes any unwanted spacing */
margin: 0 auto; /* Centers the image if container is wider */
}
.item-name {
text-align: center;
margin: 10px 0;
font-weight: bold;
font-size: 1.1em;
}
.description {
display: flex;
justify-content: space-between;
padding: 0 10px 10px 10px;
font-size: 0.9em;
}
.desc-text {
text-align: left;
}
.size-text {
text-align: right;
}
.align-center {
text-align: center;
}
.play-button {
margin-top: auto;
margin-bottom: 2px;
padding: 15px 30px;
background: linear-gradient(45deg, #09305a, #37529c);
font-size: 18px;
font-weight: bold;
text-align: center;
text-decoration: none;
border-radius: 25px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 8px 15px rgba(0, 191, 255, 0.2);
}
.play-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6);
border: 1px solid rgba(0, 191, 255, 0.6);
animation: flicker 0.2s infinite alternate;
transform: translateY(-2px);
}
@keyframes flicker {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
.search-container {
display: flex;
justify-content: center;
}
.search-input {
width: 100%;
max-width: 400px;
padding: 10px 15px;
font-size: 16px;
color: #D5DBFE;
background: #000938;
border: 1px solid #444;
border-radius: 25px;
outline: none;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: #4866b9;
box-shadow: 0 0 10px rgba(0, 191, 255, 0.2);
}
.search-input::placeholder {
color: #8892b0;
}
.search-settings-wrapper {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 20px;
}
.settings-container {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 15px;
}
.settings-button {
padding: 8px 16px;
background: linear-gradient(45deg, #09305a, #37529c);
color: #D5DBFE;
border: 1px solid transparent;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 191, 255, 0.2);
}
.settings-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
border: 1px solid rgba(0, 191, 255, 0.6);
transform: translateY(-2px);
}
.settings-text {
color: #8892b0;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}

View File

@ -0,0 +1,279 @@
import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog';
import { load } from '@tauri-apps/plugin-store';
import "./App.css";
const FILE_STORAGE = 'launcher-settings.json';
const GAME_DIR_KEY = 'game-directory';
// enum with install status
enum InstallStatus {
NotInstalled = 'NotInstalled',
CheckingServer = 'CheckingServer',
Downloading = 'Downloading',
Unpacking = 'Unpacking',
Installed = 'Installed',
}
interface Game {
id: string;
name: string;
description: string;
size: number;
thumbnail: Uint8Array;
installed: boolean;
install_status: InstallStatus;
}
const App = () => {
const [gameItems, setGameItems] = useState<Game[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [gameDir, setGameDir] = useState('');
const filteredGames = gameItems.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const getInitialGameDir = async () => {
// update game directory from storage (if exists)
// only if it's not already set
await new Promise(resolve => setTimeout(resolve, 1000));
const store = await load(FILE_STORAGE, { autoSave: true });
const savedGameDir = await store.get<string>(GAME_DIR_KEY);
if (savedGameDir) {
setGameDir(savedGameDir);
}
};
useEffect(() => {
// 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(() => {
console.log('🔵 Effect starting - setting up listener and requesting games');
const setupEventListener = async () => {
try {
// Listen for games-list-updated events
const unlisten_games = await listen('games-list-updated', (event) => {
console.log('🗲 Received games-list-updated event');
const games = event.payload as Game[];
console.log(`🎮 ${games.length} Games received`);
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
console.log('📤 Requesting initial games list');
await invoke('request_games');
// Cleanup function
return () => {
console.log('🧹 Cleaning up - removing listener');
unlisten_games();
unlisten_game_download_begin();
unlisten_game_download_finished();
};
} catch (error) {
console.error('❌ Error in setup:', error);
}
};
setupEventListener();
// Cleanup
return () => {
console.log('🚫 Effect cleanup - component unmounting');
};
}, []); // Empty dependency array means this runs once on mount
const runGame = async (id: string) => {
console.log(`🎯 Running game with id=${id}`);
try {
const result = await invoke('run_game', { id });
console.log(`✅ Game started, result=${result}`);
} catch (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
return (
<main className="container">
<div className="fixed-header">
<h1 className="align-center">SoftLAN Launcher</h1>
<div className="main-header">
{gameItems.length > 0 ? (
<div className="search-settings-wrapper">
<div></div>
<div className="search-container">
<input
type="text"
placeholder="Search games..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="settings-container">
<button onClick={dialogGameDir} className="settings-button">Set Game Directory</button>
<span className="settings-text">{gameDir}</span>
</div>
</div>
) : (
<div className="search-container">
Waiting for connection to server...
</div>
)}
</div>
</div>
<div className="grid-container">
{filteredGames.map((item) => {
const uint8Array = new Uint8Array(item.thumbnail);
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`;
return (
<div key={item.id} className="item">
<img src={thumbnailUrl} alt={`${item.name} thumbnail`} />
<div className="item-name">{item.name}</div>
<div className="description">
<span className="desc-text">{item.description.slice(0, 10)}</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>
</main>
);
};
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,6 @@
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<App />
);

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

View File

@ -1,3 +0,0 @@
/dist/
/target/
/Cargo.lock

View File

@ -1,3 +0,0 @@
/src
/public
/Cargo.toml

View File

@ -1,5 +0,0 @@
{
"emmet.includeLanguages": {
"rust": "html"
}
}

View File

@ -1,15 +0,0 @@
[package]
name = "lanspread-tauri-leptos-ui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lanspread-db = { path = "../lanspread-db" }
leptos = { version = "0.6", features = ["csr"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = "0.1.7"

View File

@ -1,7 +0,0 @@
# Tauri + Leptos
This template should help get you started developing with Tauri and Leptos.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).

View File

@ -1,9 +0,0 @@
[build]
target = "./index.html"
[watch]
ignore = ["./src-tauri"]
[serve]
port = 1420
open = false

View File

@ -1,11 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Tauri + Leptos App</title>
<link data-trunk rel="css" href="styles.css" />
<link data-trunk rel="copy-dir" href="public" />
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
<path style="fill:none;" d="M130.0327,79.3931c-11.4854-0.23-22.52,9.3486-24.5034,21.0117l49.1157,0.0293
c-2.1729-10.418-11.1821-21.0449-24.1987-21.0449C130.3081,79.3892,130.1714,79.3907,130.0327,79.3931z"/>
<path style="fill:#181139;" d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187
c-1.3159,0-2.2349,1.0005-2.2349,2.4331v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333
v-2.7744C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
<path style="fill:#181139;" d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
<path style="fill:#181139;" d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115
c4.938-2.9014,8.75-6.7129,11.6533-11.6533c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746
c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125
c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834
h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667
c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591
c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605
c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354
c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955
c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
<path style="fill:#181139;" d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189
c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371
c0,0-0.001,0-0.002,0l-1.7871-0.0488c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352
V80.3975h10.0928c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
<path style="fill:#181139;" d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646
c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146
c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523
c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282
c-11.7305,0-19.6123,6.9263-19.6123,17.2349c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377
c4.5859,1.085,8.3193,2.5654,11.0977,4.4023c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005
c0.0742,2.3857-0.79,4.5176-2.5684,6.3389c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0
c-3.4268,0-6.4893-0.8438-9.1035-2.5068c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738
c-0.0742,0-0.2109,0.0146-0.4062,0.0449c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332
c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447
c0.125,0.002,0.249,0.002,0.374,0.002c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357
c0-2.8187-0.6185-5.3109-1.8062-7.481C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
<path style="fill:#EF3939;" d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756
c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123
c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683
c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -1,127 +0,0 @@
use std::{net::SocketAddr, process::Command};
use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
use tauri::{AppHandle, Emitter as _, Manager};
use tokio::sync::{mpsc::UnboundedSender, Mutex};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
struct LanSpreadState {
server_addr: Mutex<Option<SocketAddr>>,
client_ctrl: UnboundedSender<ClientCommand>,
}
#[tauri::command]
fn request_games(state: tauri::State<LanSpreadState>) {
log::debug!("request_games");
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::ListGames) {
log::error!("Failed to send message to client: {e:?}");
}
}
#[tauri::command]
fn run_game_backend(id: u64, state: tauri::State<LanSpreadState>) -> String {
log::error!("Running game with id {id}");
let result = Command::new(r#"C:\Users\ddidderr\scoop\apps\mpv\0.39.0\mpv.exe"#).spawn();
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) {
log::error!("Failed to send message to client: {e:?}");
}
if result.is_ok() {
"Ok".to_string()
} else {
"Failed to run game".to_string()
}
}
async fn find_server(app: AppHandle) {
log::error!("Looking for server...");
loop {
match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) {
Ok(server_addr) => {
log::info!("Found server at {server_addr}");
let state: tauri::State<LanSpreadState> = app.state();
{
// mutex scope
let mut addr = state.server_addr.lock().await;
*addr = Some(server_addr);
}
state
.client_ctrl
.send(ClientCommand::ServerAddr(server_addr))
.unwrap();
request_games(state);
break;
}
Err(e) => {
log::warn!("Failed to find server: {e} - retrying...");
}
}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let tauri_logger_builder = tauri_plugin_log::Builder::new()
.clear_targets()
.target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::Stdout,
))
.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);
// channel to pass commands to the client
let (tx_client_control, rx_client_control) =
tokio::sync::mpsc::unbounded_channel::<ClientCommand>();
// channel to receive events from the client
let (tx_client_event, mut rx_client_event) =
tokio::sync::mpsc::unbounded_channel::<ClientEvent>();
let lanspread_state = LanSpreadState {
server_addr: Mutex::new(None),
client_ctrl: tx_client_control,
};
tauri::Builder::default()
.plugin(tauri_logger_builder.build())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![run_game_backend, request_games])
.manage(lanspread_state)
.setup(|app| {
let app_handle = app.handle().clone();
// discover server
tauri::async_runtime::spawn(async move { find_server(app_handle).await });
tauri::async_runtime::spawn(async move {
lanspread_client::run(rx_client_control, tx_client_event).await
});
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
while let Some(event) = rx_client_event.recv().await {
log::debug!("Received client event: {event:?}");
match event {
ClientEvent::ListGames(games) => {
if let Err(e) = app_handle.emit("games-list-updated", Some(games)) {
log::error!("Failed to emit games-list-updated event: {e}");
} else {
log::info!("Emitted games-list-updated event");
}
}
}
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,105 +0,0 @@
use js_sys::Function;
use lanspread_db::db::Game;
use leptos::*;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use web_sys::console::log_1;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"])]
async fn listen(event: &str, handler: &Function) -> JsValue;
}
#[derive(Serialize, Deserialize)]
struct RunGameArgs {
id: u64,
}
#[derive(Serialize, Deserialize)]
enum CommandArgs {
RunGame(RunGameArgs),
}
#[component]
pub fn App() -> impl IntoView {
let (game_items, set_game_items) = create_signal(Vec::<Game>::new());
// Something that listens to the global app state and updates the game_items signal
create_effect(move |_| {
spawn_local(async move {
// Convert the callback to a JS function that can be passed to listen()
let callback = Closure::wrap(Box::new(move |event: JsValue| {
log_1(&JsValue::from_str("Received games-list-updated event"));
// Access the payload property of the event
let payload = js_sys::Reflect::get(&event, &JsValue::from_str("payload")).unwrap();
// Parse the event payload
match serde_wasm_bindgen::from_value::<Vec<Game>>(payload) {
Ok(games) => {
for game in &games {
log_1(&JsValue::from_str(format!("game: {game}").as_str()));
}
set_game_items.update(|items| *items = games);
}
Err(e) => log_1(&JsValue::from_str(
format!("Error parsing games-list-updated event: {e}").as_str(),
)),
}
}) as Box<dyn FnMut(JsValue)>);
// Register the event listener
listen("games-list-updated", callback.as_ref().unchecked_ref()).await;
callback.forget(); // Prevent callback from being dropped
});
});
// Call list_games on component mount
create_effect(move |_| {
spawn_local(async move {
let args = serde_wasm_bindgen::to_value(&()).unwrap();
invoke("request_games", args).await;
});
});
let run_game = move |id: u64| {
log_1(&JsValue::from_str(format!("id={id}").as_str()));
spawn_local(async move {
let args = serde_wasm_bindgen::to_value(&RunGameArgs { id }).unwrap();
let result = invoke("run_game_backend", args).await.as_string().unwrap();
log_1(&JsValue::from_str(format!("id={result}").as_str()));
});
};
view! {
<main class="container">
<h1 class="align-center">{"SotLAN Launcher"}</h1>
<div class="main-header">
{"HEADER"}
</div>
<div class="grid-container">
{move || game_items.get().into_iter()
.map(|item|
view! {
<div class="item" on:click=move |_| run_game(item.id)>
<img src="https://via.placeholder.com/200x150" alt="Item Image" />
<div class="item-name">{ &item.name }</div>
<div class="description">
<span class="desc-text">{ &item.description }</span>
<span class="size-text">{ &item.size.to_string() }</span>
</div>
<div class="play-button">{"Play"}</div>
</div>
}
).collect::<Vec<_>>()
}
</div>
</main>
}
}

View File

@ -1,13 +0,0 @@
mod app;
use app::*;
use leptos::*;
fn main() {
console_error_panic_hook::set_once();
mount_to_body(|| {
view! {
<App/>
}
})
}

View File

@ -1,95 +0,0 @@
body {
background-color: #000313;
font-family: Arial, sans-serif;
color: #D5DBFE;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
padding: 20px;
}
.item {
background: linear-gradient(to bottom, black, #000938);
color: white;
border: 1px solid #444;
border-radius: 8px;
overflow: hidden;
transition: background 0.3s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
cursor: pointer;
}
.item:hover {
background: linear-gradient(to bottom, black, #3849AB);
}
.item img {
width: 100%;
height: 150px;
object-fit: cover;
}
.item-name {
text-align: center;
margin: 10px 0;
font-weight: bold;
font-size: 1.1em;
}
.description {
display: flex;
justify-content: space-between;
padding: 0 10px 10px 10px;
font-size: 0.9em;
}
.desc-text {
text-align: left;
}
.size-text {
text-align: right;
}
.align-center {
text-align: center;
}
.play-button {
margin-bottom: 2px;
padding: 15px 30px;
background: linear-gradient(45deg, #09305a, #37529c);
font-size: 18px;
font-weight: bold;
text-align: center;
text-decoration: none;
border-radius: 25px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 8px 15px rgba(0, 191, 255, 0.2);
}
/* .play-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
/* box-shadow: 0 15px 20px rgba(0, 191, 255, 0.4); */
/* box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6), 0 0 15px rgba(0, 191, 255, 0.5);
transform: translateY(-5px);
} */
.play-button:hover {
background: linear-gradient(45deg, #09305a, #4866b9);
box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6);
border: 1px solid rgba(0, 191, 255, 0.6);
animation: flicker 0.2s infinite alternate;
transform: translateY(-2px);
}
@keyframes flicker {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}

View File

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

View File

@ -1,6 +1,8 @@
#[macro_export]
macro_rules! maybe_addr {
($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-----
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
4g8cERBIkBP2otGtuFoA4oIQ6qsh
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
IKQxaY3Y5s7XzzFJ1Tg3RjHekKHcVw==
-----END PRIVATE KEY-----

View File

@ -1,6 +1,4 @@
#!/usr/bin/env bash
#export RUST_LOG+=,lanspread_server=debug,lanspread_proto=debug
export RUST_LOG=info,lanspread=debug
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()