Compare commits

...

21 Commits

Author SHA1 Message Date
ac11f91d79 games_in_download: Mutex -> RwLock 2025-08-27 21:29:55 +02:00
8e76e8d1e2 introduce cargo vet 2025-08-27 21:21:01 +02:00
3b6fc80578 [deps] cargo update 2025-08-27 19:55:56 +02:00
8c6fd139c8 [deps] cargo update 2025-08-20 08:54:56 +02:00
66572e16ce [deps] cargo update
Updating anyhow         v1.0.98  -> v1.0.99
Updating async-trait    v0.1.88  -> v0.1.89
Updating bitflags       v2.9.1   -> v2.9.2
Updating brotli         v8.0.1   -> v8.0.2
Updating cc             v1.2.32  -> v1.2.33
Updating clap_builder   v4.5.43  -> v4.5.44
Updating clap_derive    v4.5.41  -> v4.5.45
Updating clap           v4.5.43  -> v4.5.45
Updating dlopen2        v0.7.0   -> v0.8.0
Updating glob           v0.3.2   -> v0.3.3
Updating libc           v0.2.174 -> v0.2.175
Updating objc2          v0.6.1   -> v0.6.2
Updating proc-macro2    v1.0.96  -> v1.0.101
Updating reqwest        v0.12.22 -> v0.12.23
Updating serde-untagged v0.1.7   -> v0.1.8
Updating syn            v2.0.104 -> v2.0.106
Updating tao            v0.34.0  -> v0.34.1
Updating thiserror-impl v2.0.12  -> v2.0.15
Updating thiserror      v2.0.12  -> v2.0.15
Updating uuid           v1.17.0  -> v1.18.0
2025-08-17 16:37:01 +02:00
3b19cb8b18 clippy: apply and fix new lints 2025-08-17 16:35:54 +02:00
61a41c7122 clippy: add same lints to all crates 2025-08-17 16:12:42 +02:00
cbad9389ee code: remove unnecessary else branch 2025-08-17 16:08:38 +02:00
02d84c4d84 code: better debug for install state 2025-08-17 16:07:06 +02:00
ca40a62ff8 clippy/fmt: just fix applied 2025-08-17 16:04:45 +02:00
3dcc0271b8 justfile: typical cargo commands 2025-08-17 16:04:09 +02:00
98126a72da [clippy] fixes 2025-08-12 08:44:59 +02:00
70548d2041 refactor(server): implement request handlers as methods
moved request handling logic into dedicated methods on the request handler struct for better organization and readability. this includes handlers for ping, list games, get game info, and invalid requests. game file data handler is added but not yet implemented.
2025-08-12 08:39:23 +02:00
13d97de515 cargo fmt 2025-08-12 08:38:12 +02:00
de6d39a8d7 justfile 2025-08-12 08:38:00 +02:00
32c7659702 [deps] upgrade 2025-08-12 08:37:25 +02:00
1b90a9baf3 [deps] cargo update 2025-07-11 18:21:12 +02:00
1ffab086ff [clippy] apply simple changes with --fix 2025-07-11 13:16:13 +02:00
b8329bd0b1 [feat] use mimalloc 2025-07-11 13:13:14 +02:00
24e6c8c0b5 [deps] cargo update 2025-07-11 13:12:07 +02:00
083dbc853b [deps] deno upgrade 2025-07-11 13:10:09 +02:00
20 changed files with 6051 additions and 1300 deletions

1451
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,8 @@ eyre = "0.6"
gethostname = "1"
itertools = "0.14"
log = "0.4"
mdns-sd = "0.13"
mdns-sd = "0.14"
mimalloc = { version = "0.1", features = ["secure"] }
s2n-quic = { version = "1", features = ["provider-event-tracing"] }
semver = "1"
serde = { version = "1", features = ["derive"] }

View File

@@ -3,6 +3,14 @@ name = "lanspread-compat"
version = "0.1.0"
edition = "2024"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[dependencies]
# local
lanspread-db = { path = "../lanspread-db" }

View File

@@ -58,6 +58,7 @@ impl From<EtiGame> for Game {
max_players: eti_game.game_maxplayers,
version: eti_game.game_version,
genre: eti_game.genre_de,
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
thumbnail: None,
installed: false,

View File

@@ -61,7 +61,7 @@ impl Eq for Game {}
impl PartialOrd for Game {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.name.cmp(&other.name))
Some(self.cmp(other))
}
}

View File

@@ -3,6 +3,14 @@ name = "lanspread-mdns"
version = "0.1.0"
edition = "2024"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[dependencies]
mdns-sd = { workspace = true }
eyre = { workspace = true }

View File

@@ -1,3 +1,5 @@
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
use std::net::SocketAddr;
use eyre::bail;
@@ -59,15 +61,15 @@ pub fn discover_service(
ServiceEvent::ServiceResolved(info) => {
tracing::trace!(?info, "mdns ServiceResolved event");
if let Some(instance_name) = instance_name {
if info.get_fullname() != format!("{}.{}", instance_name, service_type) {
tracing::warn!(
"Found service with wrong instance name: {} (expected: {})",
info.get_fullname(),
instance_name,
);
continue;
}
if let Some(instance_name) = instance_name
&& info.get_fullname() != format!("{instance_name}.{service_type}")
{
tracing::warn!(
"Found service with wrong instance name: {} (expected: {})",
info.get_fullname(),
instance_name,
);
continue;
}
if let Some(address) = info.get_addresses().iter().next() {

View File

@@ -26,6 +26,7 @@ clap = { workspace = true }
eyre = { workspace = true }
gethostname = { workspace = true }
itertools = { workspace = true }
mimalloc = { workspace = true }
s2n-quic = { workspace = true }
serde_json = { workspace = true }
semver = { workspace = true }

View File

@@ -1,3 +1,8 @@
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
mod cli;
mod quic;
mod req;
@@ -11,7 +16,10 @@ use gethostname::gethostname;
use lanspread_compat::eti;
use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{
DaemonEvent, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser,
DaemonEvent,
LANSPREAD_INSTANCE_NAME,
LANSPREAD_SERVICE_TYPE,
MdnsAdvertiser,
};
use tracing_subscriber::EnvFilter;
use uuid::Uuid;

View File

@@ -44,63 +44,77 @@ impl RequestHandler {
Ok(())
}
fn handle_ping() -> Response {
Response::Pong
}
async fn handle_list_games(&self) -> Response {
let db = self.db.read().await;
Response::ListGames(db.all_games().into_iter().cloned().collect())
}
async fn handle_get_game(&self, id: String, games_folder: &Path) -> Response {
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,
}
}
fn handle_get_game_file_data() -> Response {
Response::InvalidRequest(Bytes::new(), "Not implemented".to_string())
}
fn handle_invalid(data: Bytes, err_msg: String) -> Response {
Response::InvalidRequest(data, err_msg)
}
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),
Request::Ping => RequestHandler::handle_ping(),
Request::ListGames => self.handle_list_games().await,
Request::GetGame { id } => self.handle_get_game(id, games_folder).await,
Request::GetGameFileData(_) => RequestHandler::handle_get_game_file_data(),
Request::Invalid(data, err_msg) => RequestHandler::handle_invalid(data, err_msg),
}
}
}

View File

@@ -1,19 +1,19 @@
{
"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:@tauri-apps/api@^2.6.0": "2.6.0",
"npm:@tauri-apps/api@^2.7.0": "2.7.0",
"npm:@tauri-apps/cli@^2.7.1": "2.7.1",
"npm:@tauri-apps/plugin-dialog@^2.3.2": "2.3.2",
"npm:@tauri-apps/plugin-shell@^2.3.0": "2.3.0",
"npm:@tauri-apps/plugin-store@^2.3.0": "2.3.0",
"npm:@types/react-dom@^19.1.7": "19.1.7_@types+react@19.1.9",
"npm:@types/react@^19.1.9": "19.1.9",
"npm:@vitejs/plugin-react@5": "5.0.0_vite@7.1.1__picomatch@4.0.3_@babel+core@7.28.0",
"npm:react-dom@^19.1.1": "19.1.1_react@19.1.1",
"npm:react@^19.1.1": "19.1.1",
"npm:typescript@^5.9.2": "5.9.2",
"npm:vite@^7.1.1": "7.1.1_picomatch@4.0.3"
},
"npm": {
"@ampproject/remapping@2.3.0": {
@@ -23,19 +23,19 @@
"@jridgewell/trace-mapping"
]
},
"@babel/code-frame@7.26.2": {
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"@babel/code-frame@7.27.1": {
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dependencies": [
"@babel/helper-validator-identifier",
"js-tokens",
"picocolors"
]
},
"@babel/compat-data@7.26.8": {
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="
"@babel/compat-data@7.28.0": {
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="
},
"@babel/core@7.26.10": {
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"@babel/core@7.28.0": {
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dependencies": [
"@ampproject/remapping",
"@babel/code-frame",
@@ -54,8 +54,8 @@
"semver"
]
},
"@babel/generator@7.26.10": {
"integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
"@babel/generator@7.28.0": {
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"dependencies": [
"@babel/parser",
"@babel/types",
@@ -64,8 +64,8 @@
"jsesc"
]
},
"@babel/helper-compilation-targets@7.26.5": {
"integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
"@babel/helper-compilation-targets@7.27.2": {
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dependencies": [
"@babel/compat-data",
"@babel/helper-validator-option",
@@ -74,15 +74,18 @@
"semver"
]
},
"@babel/helper-module-imports@7.25.9": {
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
"@babel/helper-globals@7.28.0": {
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="
},
"@babel/helper-module-imports@7.27.1": {
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dependencies": [
"@babel/traverse",
"@babel/types"
]
},
"@babel/helper-module-transforms@7.26.0_@babel+core@7.26.10": {
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
"@babel/helper-module-transforms@7.27.3_@babel+core@7.28.0": {
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
"dependencies": [
"@babel/core",
"@babel/helper-module-imports",
@@ -90,151 +93,153 @@
"@babel/traverse"
]
},
"@babel/helper-plugin-utils@7.26.5": {
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="
"@babel/helper-plugin-utils@7.27.1": {
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="
},
"@babel/helper-string-parser@7.25.9": {
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="
"@babel/helper-string-parser@7.27.1": {
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
},
"@babel/helper-validator-identifier@7.25.9": {
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="
"@babel/helper-validator-identifier@7.27.1": {
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="
},
"@babel/helper-validator-option@7.25.9": {
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="
"@babel/helper-validator-option@7.27.1": {
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="
},
"@babel/helpers@7.26.10": {
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"@babel/helpers@7.28.2": {
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"dependencies": [
"@babel/template",
"@babel/types"
]
},
"@babel/parser@7.26.10": {
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"@babel/parser@7.28.0": {
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dependencies": [
"@babel/types"
]
},
"@babel/plugin-transform-react-jsx-self@7.25.9_@babel+core@7.26.10": {
"integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
"@babel/plugin-transform-react-jsx-self@7.27.1_@babel+core@7.28.0": {
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"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==",
"@babel/plugin-transform-react-jsx-source@7.27.1_@babel+core@7.28.0": {
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dependencies": [
"@babel/core",
"@babel/helper-plugin-utils"
]
},
"@babel/template@7.26.9": {
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"@babel/template@7.27.2": {
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dependencies": [
"@babel/code-frame",
"@babel/parser",
"@babel/types"
]
},
"@babel/traverse@7.26.10": {
"integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==",
"@babel/traverse@7.28.0": {
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"dependencies": [
"@babel/code-frame",
"@babel/generator",
"@babel/helper-globals",
"@babel/parser",
"@babel/template",
"@babel/types",
"debug",
"globals"
"debug"
]
},
"@babel/types@7.26.10": {
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"@babel/types@7.28.2": {
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dependencies": [
"@babel/helper-string-parser",
"@babel/helper-validator-identifier"
]
},
"@esbuild/aix-ppc64@0.25.1": {
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="
"@esbuild/aix-ppc64@0.25.8": {
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="
},
"@esbuild/android-arm64@0.25.1": {
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="
"@esbuild/android-arm64@0.25.8": {
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w=="
},
"@esbuild/android-arm@0.25.1": {
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="
"@esbuild/android-arm@0.25.8": {
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw=="
},
"@esbuild/android-x64@0.25.1": {
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="
"@esbuild/android-x64@0.25.8": {
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA=="
},
"@esbuild/darwin-arm64@0.25.1": {
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="
"@esbuild/darwin-arm64@0.25.8": {
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw=="
},
"@esbuild/darwin-x64@0.25.1": {
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="
"@esbuild/darwin-x64@0.25.8": {
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg=="
},
"@esbuild/freebsd-arm64@0.25.1": {
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="
"@esbuild/freebsd-arm64@0.25.8": {
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA=="
},
"@esbuild/freebsd-x64@0.25.1": {
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="
"@esbuild/freebsd-x64@0.25.8": {
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw=="
},
"@esbuild/linux-arm64@0.25.1": {
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="
"@esbuild/linux-arm64@0.25.8": {
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w=="
},
"@esbuild/linux-arm@0.25.1": {
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="
"@esbuild/linux-arm@0.25.8": {
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg=="
},
"@esbuild/linux-ia32@0.25.1": {
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="
"@esbuild/linux-ia32@0.25.8": {
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg=="
},
"@esbuild/linux-loong64@0.25.1": {
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="
"@esbuild/linux-loong64@0.25.8": {
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ=="
},
"@esbuild/linux-mips64el@0.25.1": {
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="
"@esbuild/linux-mips64el@0.25.8": {
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw=="
},
"@esbuild/linux-ppc64@0.25.1": {
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="
"@esbuild/linux-ppc64@0.25.8": {
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ=="
},
"@esbuild/linux-riscv64@0.25.1": {
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="
"@esbuild/linux-riscv64@0.25.8": {
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg=="
},
"@esbuild/linux-s390x@0.25.1": {
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="
"@esbuild/linux-s390x@0.25.8": {
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg=="
},
"@esbuild/linux-x64@0.25.1": {
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="
"@esbuild/linux-x64@0.25.8": {
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ=="
},
"@esbuild/netbsd-arm64@0.25.1": {
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="
"@esbuild/netbsd-arm64@0.25.8": {
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw=="
},
"@esbuild/netbsd-x64@0.25.1": {
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="
"@esbuild/netbsd-x64@0.25.8": {
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg=="
},
"@esbuild/openbsd-arm64@0.25.1": {
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="
"@esbuild/openbsd-arm64@0.25.8": {
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ=="
},
"@esbuild/openbsd-x64@0.25.1": {
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="
"@esbuild/openbsd-x64@0.25.8": {
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ=="
},
"@esbuild/sunos-x64@0.25.1": {
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="
"@esbuild/openharmony-arm64@0.25.8": {
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg=="
},
"@esbuild/win32-arm64@0.25.1": {
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="
"@esbuild/sunos-x64@0.25.8": {
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w=="
},
"@esbuild/win32-ia32@0.25.1": {
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="
"@esbuild/win32-arm64@0.25.8": {
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ=="
},
"@esbuild/win32-x64@0.25.1": {
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="
"@esbuild/win32-ia32@0.25.8": {
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg=="
},
"@jridgewell/gen-mapping@0.3.8": {
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"@esbuild/win32-x64@0.25.8": {
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="
},
"@jridgewell/gen-mapping@0.3.12": {
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"dependencies": [
"@jridgewell/set-array",
"@jridgewell/sourcemap-codec",
"@jridgewell/trace-mapping"
]
@@ -242,117 +247,127 @@
"@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.4": {
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="
},
"@jridgewell/sourcemap-codec@1.5.0": {
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"@jridgewell/trace-mapping@0.3.25": {
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"@jridgewell/trace-mapping@0.3.29": {
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"dependencies": [
"@jridgewell/resolve-uri",
"@jridgewell/sourcemap-codec"
]
},
"@rollup/rollup-android-arm-eabi@4.36.0": {
"integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w=="
"@rolldown/pluginutils@1.0.0-beta.30": {
"integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw=="
},
"@rollup/rollup-android-arm64@4.36.0": {
"integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg=="
"@rollup/rollup-android-arm-eabi@4.46.2": {
"integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="
},
"@rollup/rollup-darwin-arm64@4.36.0": {
"integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw=="
"@rollup/rollup-android-arm64@4.46.2": {
"integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ=="
},
"@rollup/rollup-darwin-x64@4.36.0": {
"integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA=="
"@rollup/rollup-darwin-arm64@4.46.2": {
"integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ=="
},
"@rollup/rollup-freebsd-arm64@4.36.0": {
"integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg=="
"@rollup/rollup-darwin-x64@4.46.2": {
"integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA=="
},
"@rollup/rollup-freebsd-x64@4.36.0": {
"integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ=="
"@rollup/rollup-freebsd-arm64@4.46.2": {
"integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg=="
},
"@rollup/rollup-linux-arm-gnueabihf@4.36.0": {
"integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg=="
"@rollup/rollup-freebsd-x64@4.46.2": {
"integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw=="
},
"@rollup/rollup-linux-arm-musleabihf@4.36.0": {
"integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg=="
"@rollup/rollup-linux-arm-gnueabihf@4.46.2": {
"integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA=="
},
"@rollup/rollup-linux-arm64-gnu@4.36.0": {
"integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A=="
"@rollup/rollup-linux-arm-musleabihf@4.46.2": {
"integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ=="
},
"@rollup/rollup-linux-arm64-musl@4.36.0": {
"integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw=="
"@rollup/rollup-linux-arm64-gnu@4.46.2": {
"integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng=="
},
"@rollup/rollup-linux-loongarch64-gnu@4.36.0": {
"integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg=="
"@rollup/rollup-linux-arm64-musl@4.46.2": {
"integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg=="
},
"@rollup/rollup-linux-powerpc64le-gnu@4.36.0": {
"integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg=="
"@rollup/rollup-linux-loongarch64-gnu@4.46.2": {
"integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA=="
},
"@rollup/rollup-linux-riscv64-gnu@4.36.0": {
"integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA=="
"@rollup/rollup-linux-ppc64-gnu@4.46.2": {
"integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw=="
},
"@rollup/rollup-linux-s390x-gnu@4.36.0": {
"integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag=="
"@rollup/rollup-linux-riscv64-gnu@4.46.2": {
"integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ=="
},
"@rollup/rollup-linux-x64-gnu@4.36.0": {
"integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ=="
"@rollup/rollup-linux-riscv64-musl@4.46.2": {
"integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw=="
},
"@rollup/rollup-linux-x64-musl@4.36.0": {
"integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ=="
"@rollup/rollup-linux-s390x-gnu@4.46.2": {
"integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA=="
},
"@rollup/rollup-win32-arm64-msvc@4.36.0": {
"integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A=="
"@rollup/rollup-linux-x64-gnu@4.46.2": {
"integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA=="
},
"@rollup/rollup-win32-ia32-msvc@4.36.0": {
"integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ=="
"@rollup/rollup-linux-x64-musl@4.46.2": {
"integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA=="
},
"@rollup/rollup-win32-x64-msvc@4.36.0": {
"integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="
"@rollup/rollup-win32-arm64-msvc@4.46.2": {
"integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g=="
},
"@tauri-apps/api@2.3.0": {
"integrity": "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="
"@rollup/rollup-win32-ia32-msvc@4.46.2": {
"integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ=="
},
"@tauri-apps/cli-darwin-arm64@2.3.1": {
"integrity": "sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw=="
"@rollup/rollup-win32-x64-msvc@4.46.2": {
"integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg=="
},
"@tauri-apps/cli-darwin-x64@2.3.1": {
"integrity": "sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA=="
"@tauri-apps/api@2.6.0": {
"integrity": "sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg=="
},
"@tauri-apps/cli-linux-arm-gnueabihf@2.3.1": {
"integrity": "sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA=="
"@tauri-apps/api@2.7.0": {
"integrity": "sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg=="
},
"@tauri-apps/cli-linux-arm64-gnu@2.3.1": {
"integrity": "sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg=="
"@tauri-apps/cli-darwin-arm64@2.7.1": {
"integrity": "sha512-j2NXQN6+08G03xYiyKDKqbCV2Txt+hUKg0a8hYr92AmoCU8fgCjHyva/p16lGFGUG3P2Yu0xiNe1hXL9ZuRMzA=="
},
"@tauri-apps/cli-linux-arm64-musl@2.3.1": {
"integrity": "sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w=="
"@tauri-apps/cli-darwin-x64@2.7.1": {
"integrity": "sha512-CdYAefeM35zKsc91qIyKzbaO7FhzTyWKsE8hj7tEJ1INYpoh1NeNNyL/NSEA3Nebi5ilugioJ5tRK8ZXG8y3gw=="
},
"@tauri-apps/cli-linux-x64-gnu@2.3.1": {
"integrity": "sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA=="
"@tauri-apps/cli-linux-arm-gnueabihf@2.7.1": {
"integrity": "sha512-dnvyJrTA1UJxJjQ8q1N/gWomjP8Twij1BUQu2fdcT3OPpqlrbOk5R1yT0oD/721xoKNjroB5BXCsmmlykllxNg=="
},
"@tauri-apps/cli-linux-x64-musl@2.3.1": {
"integrity": "sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA=="
"@tauri-apps/cli-linux-arm64-gnu@2.7.1": {
"integrity": "sha512-FtBW6LJPNRTws3qyUc294AqCWU91l/H0SsFKq6q4Q45MSS4x6wxLxou8zB53tLDGEPx3JSoPLcDaSfPlSbyujQ=="
},
"@tauri-apps/cli-win32-arm64-msvc@2.3.1": {
"integrity": "sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww=="
"@tauri-apps/cli-linux-arm64-musl@2.7.1": {
"integrity": "sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA=="
},
"@tauri-apps/cli-win32-ia32-msvc@2.3.1": {
"integrity": "sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw=="
"@tauri-apps/cli-linux-riscv64-gnu@2.7.1": {
"integrity": "sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg=="
},
"@tauri-apps/cli-win32-x64-msvc@2.3.1": {
"integrity": "sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q=="
"@tauri-apps/cli-linux-x64-gnu@2.7.1": {
"integrity": "sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA=="
},
"@tauri-apps/cli@2.3.1": {
"integrity": "sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==",
"@tauri-apps/cli-linux-x64-musl@2.7.1": {
"integrity": "sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA=="
},
"@tauri-apps/cli-win32-arm64-msvc@2.7.1": {
"integrity": "sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA=="
},
"@tauri-apps/cli-win32-ia32-msvc@2.7.1": {
"integrity": "sha512-1oeibfyWQPVcijOrTg709qhbXArjX3x1MPjrmA5anlygwrbByxLBcLXvotcOeULFcnH2FYUMMLLant8kgvwE5A=="
},
"@tauri-apps/cli-win32-x64-msvc@2.7.1": {
"integrity": "sha512-D7Q9kDObutuirCNLxYQ7KAg2Xxg99AjcdYz/KuMw5HvyEPbkC9Q7JL0vOrQOrHEHxIQ2lYzFOZvKKoC2yyqXcg=="
},
"@tauri-apps/cli@2.7.1": {
"integrity": "sha512-RcGWR4jOUEl92w3uvI0h61Llkfj9lwGD1iwvDRD2isMrDhOzjeeeVn9aGzeW1jubQ/kAbMYfydcA4BA0Cy733Q==",
"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-riscv64-gnu",
"@tauri-apps/cli-linux-x64-gnu",
"@tauri-apps/cli-linux-x64-musl",
"@tauri-apps/cli-win32-arm64-msvc",
@@ -360,22 +375,22 @@
"@tauri-apps/cli-win32-x64-msvc"
]
},
"@tauri-apps/plugin-dialog@2.2.0": {
"integrity": "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==",
"@tauri-apps/plugin-dialog@2.3.2": {
"integrity": "sha512-cNLo9YeQSC0MF4IgXnotHsqEgJk72MBZLXmQPrLA95qTaaWiiaFQ38hIMdZ6YbGUNkr3oni3EhU+AD5jLHcdUA==",
"dependencies": [
"@tauri-apps/api"
"@tauri-apps/api@2.7.0"
]
},
"@tauri-apps/plugin-shell@2.2.0": {
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
"@tauri-apps/plugin-shell@2.3.0": {
"integrity": "sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==",
"dependencies": [
"@tauri-apps/api"
"@tauri-apps/api@2.6.0"
]
},
"@tauri-apps/plugin-store@2.2.0": {
"integrity": "sha512-hJTRtuJis4w5fW1dkcgftsYxKXK0+DbAqurZ3CURHG5WkAyyZgbxpeYctw12bbzF9ZbZREXZklPq8mocCC3Sgg==",
"@tauri-apps/plugin-store@2.3.0": {
"integrity": "sha512-mre8er0nXPhyEWQzWCpUd+UnEoBQYcoA5JYlwpwOV9wcxKqlXTGfminpKsE37ic8NUb2BIZqf0QQ9/U3ib2+/A==",
"dependencies": [
"@tauri-apps/api"
"@tauri-apps/api@2.6.0"
]
},
"@types/babel__core@7.20.5": {
@@ -388,8 +403,8 @@
"@types/babel__traverse"
]
},
"@types/babel__generator@7.6.8": {
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
"@types/babel__generator@7.27.0": {
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dependencies": [
"@babel/types"
]
@@ -401,46 +416,41 @@
"@babel/types"
]
},
"@types/babel__traverse@7.20.6": {
"integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
"@types/babel__traverse@7.28.0": {
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dependencies": [
"@babel/types"
]
},
"@types/estree@1.0.6": {
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
"@types/estree@1.0.8": {
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
},
"@types/react-dom@19.0.4_@types+react@19.0.10": {
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"@types/react-dom@19.1.7_@types+react@19.1.9": {
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"dependencies": [
"@types/react@19.0.10"
"@types/react"
]
},
"@types/react@19.0.10": {
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"@types/react@19.1.9": {
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
"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==",
"@vitejs/plugin-react@5.0.0_vite@7.1.1__picomatch@4.0.3_@babel+core@7.28.0": {
"integrity": "sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==",
"dependencies": [
"@babel/core",
"@babel/plugin-transform-react-jsx-self",
"@babel/plugin-transform-react-jsx-source",
"@rolldown/pluginutils",
"@types/babel__core",
"react-refresh",
"vite"
]
},
"browserslist@4.24.4": {
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"browserslist@4.25.2": {
"integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
"dependencies": [
"caniuse-lite",
"electron-to-chromium",
@@ -448,8 +458,8 @@
"update-browserslist-db"
]
},
"caniuse-lite@1.0.30001706": {
"integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug=="
"caniuse-lite@1.0.30001733": {
"integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q=="
},
"convert-source-map@2.0.0": {
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
@@ -457,17 +467,17 @@
"csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"debug@4.4.0": {
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"debug@4.4.1": {
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dependencies": [
"ms"
]
},
"electron-to-chromium@1.5.122": {
"integrity": "sha512-EML1wnwkY5MFh/xUnCvY8FrhUuKzdYhowuZExZOfwJo+Zu9OsNCI23Cgl5y7awy7HrUHSwB1Z8pZX5TI34lsUg=="
"electron-to-chromium@1.5.199": {
"integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ=="
},
"esbuild@0.25.1": {
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"esbuild@0.25.8": {
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"dependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
@@ -490,6 +500,7 @@
"@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64",
"@esbuild/openharmony-arm64",
"@esbuild/sunos-x64",
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
@@ -499,15 +510,18 @@
"escalade@3.2.0": {
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
},
"fdir@6.4.6_picomatch@4.0.3": {
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"dependencies": [
"picomatch"
]
},
"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=="
},
@@ -535,29 +549,32 @@
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"postcss@8.5.3": {
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"picomatch@4.0.3": {
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
},
"postcss@8.5.6": {
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dependencies": [
"nanoid",
"picocolors",
"source-map-js"
]
},
"react-dom@19.0.0_react@19.0.0": {
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"react-dom@19.1.1_react@19.1.1": {
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"dependencies": [
"react",
"scheduler"
]
},
"react-refresh@0.14.2": {
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="
"react-refresh@0.17.0": {
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="
},
"react@19.0.0": {
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="
"react@19.1.1": {
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="
},
"rollup@4.36.0": {
"integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==",
"rollup@4.46.2": {
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"dependencies": [
"@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64",
@@ -570,8 +587,9 @@
"@rollup/rollup-linux-arm64-gnu",
"@rollup/rollup-linux-arm64-musl",
"@rollup/rollup-linux-loongarch64-gnu",
"@rollup/rollup-linux-powerpc64le-gnu",
"@rollup/rollup-linux-ppc64-gnu",
"@rollup/rollup-linux-riscv64-gnu",
"@rollup/rollup-linux-riscv64-musl",
"@rollup/rollup-linux-s390x-gnu",
"@rollup/rollup-linux-x64-gnu",
"@rollup/rollup-linux-x64-musl",
@@ -582,8 +600,8 @@
"fsevents"
]
},
"scheduler@0.25.0": {
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
"scheduler@0.26.0": {
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
},
"semver@6.3.1": {
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
@@ -591,10 +609,17 @@
"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=="
"tinyglobby@0.2.14_picomatch@4.0.3": {
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dependencies": [
"fdir",
"picomatch"
]
},
"update-browserslist-db@1.1.3_browserslist@4.24.4": {
"typescript@5.9.2": {
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="
},
"update-browserslist-db@1.1.3_browserslist@4.25.2": {
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dependencies": [
"browserslist",
@@ -602,13 +627,16 @@
"picocolors"
]
},
"vite@6.2.2": {
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"vite@7.1.1_picomatch@4.0.3": {
"integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
"dependencies": [
"esbuild",
"fdir",
"fsevents",
"picomatch",
"postcss",
"rollup"
"rollup",
"tinyglobby"
]
},
"yallist@3.1.1": {
@@ -618,18 +646,18 @@
"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"
"npm:@tauri-apps/api@^2.7.0",
"npm:@tauri-apps/cli@^2.7.1",
"npm:@tauri-apps/plugin-dialog@^2.3.2",
"npm:@tauri-apps/plugin-shell@^2.3.0",
"npm:@tauri-apps/plugin-store@^2.3.0",
"npm:@types/react-dom@^19.1.7",
"npm:@types/react@^19.1.9",
"npm:@vitejs/plugin-react@5",
"npm:react-dom@^19.1.1",
"npm:react@^19.1.1",
"npm:typescript@^5.9.2",
"npm:vite@^7.1.1"
]
}
}

View File

@@ -10,19 +10,19 @@
"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"
"@tauri-apps/plugin-dialog": "^2.3.2",
"@tauri-apps/plugin-store": "^2.3.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-shell": "^2.3.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"
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "^5.9.2",
"vite": "^7.1.1",
"@tauri-apps/cli": "^2.7.1"
}
}

View File

@@ -14,6 +14,15 @@ edition = "2024"
name = "lanspread_tauri_deno_ts_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
needless_pass_by_value = "allow"
[build-dependencies]
tauri-build = { version = "2", features = [] }
@@ -26,6 +35,7 @@ lanspread-mdns = { path = "../../lanspread-mdns" }
# external
eyre = { workspace = true }
log = { workspace = true }
mimalloc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build();
}

View File

@@ -1,485 +1,482 @@
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");
}
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::{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<RwLock<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.read().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()
&& let Some(file_name) = file_name.to_str()
&& let Some(game) = game_db.get_mut_game_by_id(file_name)
{
if installed {
log::debug!("Set {game} to installed");
} else {
log::debug!("Set {game} to uninstalled");
}
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()))
.expect("Failed to send ClientCommand: SetGameDir");
{
tauri::async_runtime::block_on(async {
let mut games_folder = app_handle
.state::<LanSpreadState>()
.inner()
.games_folder
.write()
.await;
games_folder.clone_from(&path);
});
}
let path = PathBuf::from(path);
if !path.exists() {
log::error!("game dir {} does not exist", path.display());
}
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
&& let Ok(path_type) = entry.file_type()
&& 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))
.expect("Failed to send ClientCommand: ServerAddr");
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(());
}
log::error!("dest_dir canonicalize failed: {}", dest_dir.display());
} else {
log::error!("rar_file canonicalize failed: {}", rar_file.display());
}
}
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!("{} -> {}: {e}", eti_rar.display(), local_path.display());
}
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::missing_panics_doc)]
#[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(RwLock::new(HashSet::new())),
games_folder: Arc::new(RwLock::new(String::new())),
};
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,
})
.expect("Failed to send ClientCommand: DownloadGameFiles");
}
ClientEvent::DownloadGameFilesBegin { id } => {
log::info!("ClientEvent::DownloadGameFilesBegin received");
app_handle
.state::<LanSpreadState>()
.inner()
.games_in_download
.write()
.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
.write()
.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
.write()
.await
.remove(&id.clone());
},
}
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -1,6 +1,11 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() {
lanspread_tauri_deno_ts_lib::run()
lanspread_tauri_deno_ts_lib::run();
}

22
justfile Normal file
View File

@@ -0,0 +1,22 @@
export RUSTFLAGS := "-C target-cpu=native"
server:
cargo build -p lanspread-server
client:
cargo tauri dev
fmt:
cargo +nightly fmt
_fix:
cargo fix
cargo clippy --fix
fix: _fix fmt
clippy:
cargo clippy
clean:
cargo clean

7
supply-chain/audits.toml Normal file
View File

@@ -0,0 +1,7 @@
# cargo-vet audits file
[[audits.windows-link]]
who = "ddidderr <ddidderr@paul.network>"
criteria = "safe-to-deploy"
delta = "0.1.1 -> 0.1.3"

2184
supply-chain/config.toml Normal file

File diff suppressed because it is too large Load Diff

1982
supply-chain/imports.lock Normal file

File diff suppressed because it is too large Load Diff