game thumbnails
@@ -1763,9 +1763,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f"
|
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1784,9 +1784,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.17"
|
version = "0.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2245,6 +2245,7 @@ dependencies = [
|
|||||||
name = "lanspread-tauri-deno-ts"
|
name = "lanspread-tauri-deno-ts"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"eyre",
|
"eyre",
|
||||||
"lanspread-compat",
|
"lanspread-compat",
|
||||||
"lanspread-db",
|
"lanspread-db",
|
||||||
@@ -4661,9 +4662,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.9.2"
|
version = "2.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5"
|
checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4712,9 +4713,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "2.5.1"
|
version = "2.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38"
|
checksum = "87d6f8cafe6a75514ce5333f115b7b1866e8e68d9672bf4ca89fc0f35697ea9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@@ -4734,9 +4735,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.5.0"
|
version = "2.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190"
|
checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -4761,9 +4762,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.5.0"
|
version = "2.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f"
|
checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ members = [
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
base64 = "0.22"
|
||||||
bytes = { version = "1", features = ["serde"] }
|
bytes = { version = "1", features = ["serde"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ impl From<EtiGame> for Game {
|
|||||||
genre: eti_game.genre_de,
|
genre: eti_game.genre_de,
|
||||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||||
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
|
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
|
||||||
thumbnail: None,
|
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
installed: false,
|
installed: false,
|
||||||
eti_game_version: None,
|
eti_game_version: None,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
use std::{collections::HashMap, fmt, path::Path};
|
use std::{collections::HashMap, fmt, path::Path};
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Read version from version.ini file
|
/// Read version from version.ini file
|
||||||
@@ -52,8 +51,6 @@ pub struct Game {
|
|||||||
pub genre: String,
|
pub genre: String,
|
||||||
/// size in bytes: example: 3455063152
|
/// size in bytes: example: 3455063152
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
/// thumbnail image
|
|
||||||
pub thumbnail: Option<Bytes>,
|
|
||||||
/// indicates that the ETI bundle exists locally
|
/// indicates that the ETI bundle exists locally
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub downloaded: bool,
|
pub downloaded: bool,
|
||||||
@@ -128,17 +125,6 @@ impl GameDB {
|
|||||||
db
|
db
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_thumbnails(&mut self, thumbs_dir: &Path) {
|
|
||||||
for game in self.games.values_mut() {
|
|
||||||
let asset = thumbs_dir.join(format!("{}.jpg", game.id));
|
|
||||||
if let Ok(data) = std::fs::read(&asset) {
|
|
||||||
game.thumbnail = Some(Bytes::from(data));
|
|
||||||
} else {
|
|
||||||
tracing::warn!("Thumbnail missing: {}", game.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
|
pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -1251,7 +1251,6 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result<GameDB> {
|
|||||||
version: "1.0".to_string(),
|
version: "1.0".to_string(),
|
||||||
genre: String::new(),
|
genre: String::new(),
|
||||||
size,
|
size,
|
||||||
thumbnail: None,
|
|
||||||
downloaded,
|
downloaded,
|
||||||
installed,
|
installed,
|
||||||
eti_game_version: local_version.clone(),
|
eti_game_version: local_version.clone(),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ lanspread-mdns = { path = "../../lanspread-mdns" }
|
|||||||
lanspread-compat = { path = "../../lanspread-compat" }
|
lanspread-compat = { path = "../../lanspread-compat" }
|
||||||
|
|
||||||
# external
|
# external
|
||||||
|
base64 = { workspace = true }
|
||||||
eyre = { workspace = true }
|
eyre = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
mimalloc = { workspace = true }
|
mimalloc = { workspace = true }
|
||||||
|
|||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
@@ -268,6 +268,23 @@ async fn get_peer_count(state: tauri::State<'_, LanSpreadState>) -> tauri::Resul
|
|||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_game_thumbnail(
|
||||||
|
game_id: String,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
) -> tauri::Result<String> {
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
|
let resource_path = app_handle.path().resolve(
|
||||||
|
format!("assets/{game_id}.jpg"),
|
||||||
|
tauri::path::BaseDirectory::Resource,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let image_data = std::fs::read(&resource_path)?;
|
||||||
|
let base64_data = base64::engine::general_purpose::STANDARD.encode(&image_data);
|
||||||
|
Ok(format!("data:image/jpeg;base64,{base64_data}"))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
|
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
|
||||||
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
|
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
|
||||||
@@ -817,7 +834,8 @@ pub fn run() {
|
|||||||
run_game,
|
run_game,
|
||||||
update_game_directory,
|
update_game_directory,
|
||||||
update_game,
|
update_game,
|
||||||
get_peer_count
|
get_peer_count,
|
||||||
|
get_game_thumbnail
|
||||||
])
|
])
|
||||||
.manage(lanspread_state)
|
.manage(lanspread_state)
|
||||||
.setup({
|
.setup({
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
"binaries/unrar"
|
"binaries/unrar"
|
||||||
],
|
],
|
||||||
"resources": [
|
"resources": [
|
||||||
"game.db"
|
"game.db",
|
||||||
|
"assets/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
@@ -9,6 +9,8 @@ import "./App.css";
|
|||||||
const FILE_STORAGE = 'launcher-settings.json';
|
const FILE_STORAGE = 'launcher-settings.json';
|
||||||
const GAME_DIR_KEY = 'game-directory';
|
const GAME_DIR_KEY = 'game-directory';
|
||||||
const CHECKING_PEERS_TIMEOUT_MS = 5000;
|
const CHECKING_PEERS_TIMEOUT_MS = 5000;
|
||||||
|
const FALLBACK_THUMBNAIL =
|
||||||
|
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A8A';
|
||||||
|
|
||||||
// enum with install status
|
// enum with install status
|
||||||
enum InstallStatus {
|
enum InstallStatus {
|
||||||
@@ -39,6 +41,39 @@ interface Game {
|
|||||||
peer_count: number;
|
peer_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GameThumbnailProps {
|
||||||
|
gameId: string;
|
||||||
|
alt: string;
|
||||||
|
getThumbnailUrl: (gameId: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameThumbnail = ({ gameId, alt, getThumbnailUrl }: GameThumbnailProps) => {
|
||||||
|
const [thumbnailUrl, setThumbnailUrl] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadThumbnail = async () => {
|
||||||
|
const url = await getThumbnailUrl(gameId);
|
||||||
|
if (isMounted) {
|
||||||
|
setThumbnailUrl(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadThumbnail();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [gameId, getThumbnailUrl]);
|
||||||
|
|
||||||
|
if (!thumbnailUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img src={thumbnailUrl} alt={alt} />;
|
||||||
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [gameItems, setGameItems] = useState<Game[]>([]);
|
const [gameItems, setGameItems] = useState<Game[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -46,6 +81,24 @@ const App = () => {
|
|||||||
const [currentFilter, setCurrentFilter] = useState<GameFilter>('available');
|
const [currentFilter, setCurrentFilter] = useState<GameFilter>('available');
|
||||||
const [totalPeerCount, setTotalPeerCount] = useState(0);
|
const [totalPeerCount, setTotalPeerCount] = useState(0);
|
||||||
const checkingPeersTimeouts = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
const checkingPeersTimeouts = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
||||||
|
const [thumbnails, setThumbnails] = useState<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
const getThumbnailUrl = useCallback(async (gameId: string): Promise<string> => {
|
||||||
|
// Check cache first
|
||||||
|
if (thumbnails.has(gameId)) {
|
||||||
|
return thumbnails.get(gameId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const thumbnailUrl = await invoke<string>('get_game_thumbnail', { gameId });
|
||||||
|
setThumbnails(prev => new Map(prev).set(gameId, thumbnailUrl));
|
||||||
|
return thumbnailUrl;
|
||||||
|
} catch {
|
||||||
|
// Return a small placeholder for missing images
|
||||||
|
setThumbnails(prev => new Map(prev).set(gameId, FALLBACK_THUMBNAIL));
|
||||||
|
return FALLBACK_THUMBNAIL;
|
||||||
|
}
|
||||||
|
}, [thumbnails]);
|
||||||
|
|
||||||
const getFilteredGames = (games: Game[], filter: GameFilter): Game[] => {
|
const getFilteredGames = (games: Game[], filter: GameFilter): Game[] => {
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
@@ -549,50 +602,49 @@ const App = () => {
|
|||||||
No games found matching your search and filters.
|
No games found matching your search and filters.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{filteredAndSearchedGames.map((item) => {
|
{filteredAndSearchedGames.map((item) => (
|
||||||
const uint8Array = new Uint8Array(item.thumbnail);
|
<div key={item.id} className="item">
|
||||||
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
|
<GameThumbnail
|
||||||
const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`;
|
gameId={item.id}
|
||||||
return (
|
alt={`${item.name} thumbnail`}
|
||||||
<div key={item.id} className="item">
|
getThumbnailUrl={getThumbnailUrl}
|
||||||
<img src={thumbnailUrl} alt={`${item.name} thumbnail`} />
|
/>
|
||||||
<div className="item-name">{item.name}</div>
|
<div className="item-name">{item.name}</div>
|
||||||
<div className="description">
|
<div className="description">
|
||||||
<span className="desc-text">{item.description.slice(0, 10)}</span>
|
<span className="desc-text">{item.description.slice(0, 10)}</span>
|
||||||
<span className="size-text">{(item.size / 1024 / 1024 / 1024).toFixed(1)} GB</span>
|
<span className="size-text">{(item.size / 1024 / 1024 / 1024).toFixed(1)} GB</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`play-button${isUnavailable(item) ? ' unavailable' : ''}`}
|
className={`play-button${isUnavailable(item) ? ' unavailable' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isUnavailable(item)) {
|
if (isUnavailable(item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.installed) {
|
if (!item.installed) {
|
||||||
installGame(item.id);
|
installGame(item.id);
|
||||||
} else if (needsUpdate(item)) {
|
} else if (needsUpdate(item)) {
|
||||||
updateGame(item.id);
|
updateGame(item.id);
|
||||||
} else {
|
} else {
|
||||||
runGame(item.id);
|
runGame(item.id);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{getActionLabel(item)}
|
{getActionLabel(item)}
|
||||||
|
</div>
|
||||||
|
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}>
|
||||||
|
<div className="status-left">
|
||||||
|
{item.status_message && item.peer_count === 0 && !item.installed && !item.downloaded ? item.status_message : ''}
|
||||||
</div>
|
</div>
|
||||||
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}>
|
<div className="status-right">
|
||||||
<div className="status-left">
|
{item.peer_count > 0 && (
|
||||||
{item.status_message && item.peer_count === 0 && !item.installed && !item.downloaded ? item.status_message : ''}
|
<span className="peer-count">
|
||||||
</div>
|
👥 {item.peer_count}
|
||||||
<div className="status-right">
|
</span>
|
||||||
{item.peer_count > 0 && (
|
)}
|
||||||
<span className="peer-count">
|
|
||||||
👥 {item.peer_count}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||