diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 692282c..70dbfe2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3737,6 +3737,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-rustls 0.24.1", + "tokio-socks", "tokio-util", "tower-service", "url", @@ -5291,6 +5292,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b676131..e275f51 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-gamepad = "0.0.5" -reqwest = { version = "0.11", default-features = false, features = ["stream", "json", "rustls-tls-webpki-roots"] } +reqwest = { version = "0.11", default-features = false, features = ["stream", "json", "rustls-tls-webpki-roots", "socks"] } tokio = { version = "1", features = ["full"] } futures-util = "0.3" tokio-util = { version = "0.7.18", features = ["rt"] } diff --git a/src-tauri/src/commands/dlc.rs b/src-tauri/src/commands/dlc.rs index 88f3f04..a0f0782 100644 --- a/src-tauri/src/commands/dlc.rs +++ b/src-tauri/src/commands/dlc.rs @@ -45,14 +45,16 @@ fn get_raw_url(host: &str, owner: &str, repo: &str, branch: &str, path: &str, is #[tauri::command] pub async fn list_git_directory( + app: tauri::AppHandle, repo_url: String, branch: String, path: String, ) -> Result, String> { - list_git_directory_inner(repo_url, branch, path).await + list_git_directory_inner(app, repo_url, branch, path).await } fn list_git_directory_inner( + app: tauri::AppHandle, repo_url: String, branch: String, path: String, @@ -60,9 +62,8 @@ fn list_git_directory_inner( Box::pin(async move { let (owner, repo, host, is_github) = parse_git_url(&repo_url)?; let api_url = get_api_url(&host, &owner, &repo, &path, &branch, is_github); - let client = reqwest::Client::new(); + let client = util::build_http_client_from_app(&app).map_err(|e| e.to_string())?; let response = client.get(&api_url) - .header("User-Agent", "Emerald-Launcher") .send() .await .map_err(|e| format!("Failed to fetch directory listing: {}", e))?; @@ -82,7 +83,7 @@ fn list_git_directory_inner( let is_dir = json.get("type").and_then(|v| v.as_str()) == Some("dir"); if is_dir { let sub_path = json.get("path").and_then(|v| v.as_str()).unwrap_or("").to_string(); - return list_git_directory_inner(repo_url, branch, sub_path).await; + return list_git_directory_inner(app, repo_url, branch, sub_path).await; } return Ok(Vec::new()); } @@ -107,6 +108,7 @@ fn list_git_directory_inner( } async fn collect_files( + app: &tauri::AppHandle, host: &str, owner: &str, repo: &str, @@ -116,11 +118,10 @@ async fn collect_files( ) -> Result, String> { let mut files = Vec::new(); let mut dirs_to_list = vec![root_path.to_string()]; - let client = reqwest::Client::new(); + let client = util::build_http_client_from_app(app).map_err(|e| e.to_string())?; while let Some(dir) = dirs_to_list.pop() { let api_url = get_api_url(host, owner, repo, &dir, branch, is_github); let response = client.get(&api_url) - .header("User-Agent", "Emerald-Launcher") .send() .await .map_err(|e| format!("Failed to list {}: {}", dir, e))?; @@ -174,18 +175,17 @@ pub async fn download_dlc_files( let instance_dir = util::get_instance_working_dir(&app, &instance_id); let dlc_dest = instance_dir.join("Windows64Media").join("DLC").join(&dlc_folder); let (owner, repo, host, is_github) = parse_git_url(&repo_url)?; - let files_to_download = collect_files(&host, &owner, &repo, &branch, &dlc_folder, is_github).await?; + let files_to_download = collect_files(&app, &host, &owner, &repo, &branch, &dlc_folder, is_github).await?; if files_to_download.is_empty() { return Err(format!("No files found in '{}' folder", dlc_folder)); } fs::create_dir_all(&dlc_dest).map_err(|e| e.to_string())?; - let client = reqwest::Client::new(); + let client = util::build_http_client_from_app(&app).map_err(|e| e.to_string())?; let total = files_to_download.len(); for (i, file_path) in files_to_download.iter().enumerate() { let raw_url = get_raw_url(&host, &owner, &repo, &branch, file_path, is_github); let response = client.get(&raw_url) - .header("User-Agent", "Emerald-Launcher") .send() .await .map_err(|e| format!("Failed to download {}: {}", file_path, e))?; diff --git a/src-tauri/src/commands/download.rs b/src-tauri/src/commands/download.rs index 7985573..d39a483 100644 --- a/src-tauri/src/commands/download.rs +++ b/src-tauri/src/commands/download.rs @@ -28,7 +28,8 @@ async fn stream_download( lock.insert(instance_id.to_string(), token); } - let response = reqwest::Client::new().get(url).header(reqwest::header::USER_AGENT, "Emerald-Launcher").send().await.map_err(|e| e.to_string())?; + let client = util::build_http_client_from_app(app).map_err(|e| e.to_string())?; + let response = client.get(url).send().await.map_err(|e| e.to_string())?; if !response.status().is_success() { { state.tokens.lock().await.remove(instance_id); } return Err(format!("Download failed: {}", response.status())); @@ -300,9 +301,9 @@ pub async fn check_game_update( return Ok(true); } - let response = reqwest::Client::new() + let client = util::build_http_client_from_app(&app).map_err(|e| e.to_string())?; + let response = client .head(&url) - .header(reqwest::header::USER_AGENT, "Emerald-Launcher") .send() .await .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/commands/macos_setup.rs b/src-tauri/src/commands/macos_setup.rs index 1dee3e3..0482891 100644 --- a/src-tauri/src/commands/macos_setup.rs +++ b/src-tauri/src/commands/macos_setup.rs @@ -15,6 +15,8 @@ use serde::Deserialize; #[cfg(target_os = "macos")] use crate::platform::macos; #[cfg(target_os = "macos")] +use crate::util; +#[cfg(target_os = "macos")] use std::fs; #[tauri::command] @@ -71,7 +73,7 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul None, ); - let client = reqwest::Client::new(); + let client = util::build_http_client_from_app(&app).map_err(|e| e.to_string())?; let release_text = client .get(format!("https://api.github.com/repos/{}/releases/latest", repo)) .header("User-Agent", "Emerald-Legacy-Launcher") diff --git a/src-tauri/src/commands/proxy_cmd.rs b/src-tauri/src/commands/proxy_cmd.rs index 5002c82..a516815 100644 --- a/src-tauri/src/commands/proxy_cmd.rs +++ b/src-tauri/src/commands/proxy_cmd.rs @@ -1,12 +1,14 @@ use crate::types::HttpResponse; +use crate::util; #[tauri::command] pub async fn http_proxy_request( + app: tauri::AppHandle, method: String, url: String, body: Option, headers: std::collections::HashMap, ) -> Result { - let client = reqwest::Client::new(); + let client = util::build_http_client_from_app(&app)?; let mut req = match method.to_uppercase().as_str() { "GET" => client.get(&url), "POST" => client.post(&url), diff --git a/src-tauri/src/commands/skin.rs b/src-tauri/src/commands/skin.rs index 2108c20..5cfbb78 100644 --- a/src-tauri/src/commands/skin.rs +++ b/src-tauri/src/commands/skin.rs @@ -7,8 +7,8 @@ use crate::types::ScreenshotInfo; use crate::config; use crate::util; #[tauri::command] -pub async fn fetch_skin(username: String) -> Result<(String, String), String> { - let client = reqwest::Client::new(); +pub async fn fetch_skin(app: AppHandle, username: String) -> Result<(String, String), String> { + let client = util::build_http_client_from_app(&app).map_err(|e| e.to_string())?; let mojang_url = format!("https://api.mojang.com/users/profiles/minecraft/{}", username); let mojang_res = client.get(&mojang_url).send().await.map_err(|e| format!("Failed request to mojang: {}", e))?; if !mojang_res.status().is_success() { @@ -47,7 +47,8 @@ pub async fn download_logo(app: AppHandle, id: String, url: String) -> Result = Vec::new(); for part in parts { let url = format!("{}/{}", raw_base, part); - let response = reqwest::get(&url).await.map_err(|e| e.to_string())?; + let client = util::build_http_client_from_app(app).map_err(|e| e.to_string())?; + let response = client.get(&url).send().await.map_err(|e| e.to_string())?; if !response.status().is_success() { return Err(format!("Failed to download {}: HTTP {}", part, response.status())); } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 566b439..b6e30d0 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -42,6 +42,7 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig { android_audio_backend: None, goldmapper_enabled: Some(true), goldmapper_mappings: None, + http_proxy: None, } } diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 49bed28..74164f2 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -77,6 +77,7 @@ pub struct AppConfig { pub android_audio_backend: Option, pub goldmapper_enabled: Option, pub goldmapper_mappings: Option>, + pub http_proxy: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/src-tauri/src/util.rs b/src-tauri/src/util.rs index 774bc9a..472eee1 100644 --- a/src-tauri/src/util.rs +++ b/src-tauri/src/util.rs @@ -44,6 +44,24 @@ pub fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> Result { + let mut builder = reqwest::Client::builder(); + builder = builder.user_agent("Emerald-Launcher"); + if let Some(p) = proxy { + let trimmed = p.trim(); + if !trimmed.is_empty() { + let proxy = reqwest::Proxy::all(trimmed).map_err(|e| format!("Invalid proxy: {e}"))?; + builder = builder.proxy(proxy); + } + } + builder.build().map_err(|e| e.to_string()) +} + +pub fn build_http_client_from_app(app: &AppHandle) -> Result { + let config = config::load_config_raw(app.clone()); + build_http_client(config.http_proxy.as_deref()) +} + #[cfg(unix)] pub fn unix_path_to_wine_z_path(unix_path: &PathBuf) -> String { let p = unix_path.to_string_lossy(); diff --git a/src-tauri/src/workshop_server.rs b/src-tauri/src/workshop_server.rs index d1fa70b..24e0382 100644 --- a/src-tauri/src/workshop_server.rs +++ b/src-tauri/src/workshop_server.rs @@ -1,10 +1,8 @@ -use once_cell::sync::Lazy; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; use tauri::{AppHandle, Emitter}; const REGISTRY_URL: &str = "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main"; //neo: more hardcoding! -static CLIENT: Lazy = Lazy::new(|| reqwest::Client::new()); pub struct Guard { cancel: Option, } @@ -48,7 +46,8 @@ async fn serve(app: AppHandle, cancel: CancellationToken) { result = listener.accept() => { match result { Ok((stream, _)) => { - tokio::spawn(handle(stream)); + let handle_app = app.clone(); + tokio::spawn(handle(stream, handle_app)); } Err(e) => { let _ = app.emit("backend-error", format!("Workshop server accept error: {e}")); @@ -61,7 +60,7 @@ async fn serve(app: AppHandle, cancel: CancellationToken) { } } -async fn handle(stream: tokio::net::TcpStream) { +async fn handle(stream: tokio::net::TcpStream, app: AppHandle) { let (reader, mut writer) = stream.into_split(); let mut buf_reader = BufReader::new(reader); let mut request_line = String::new(); @@ -81,7 +80,7 @@ async fn handle(stream: tokio::net::TcpStream) { if request_line.starts_with("GET /workshop/") && request_line.ends_with(" HTTP/1.1") { let path = &request_line["GET /workshop/".len()..request_line.len() - " HTTP/1.1".len()]; - match fetch_workshop_file(path).await { + match fetch_workshop_file(&app, path).await { Ok(body) => { let body_bytes = &body; let content_type = if path.ends_with(".json") { @@ -123,9 +122,10 @@ async fn handle(stream: tokio::net::TcpStream) { } } -async fn fetch_workshop_file(path: &str) -> Result, String> { +async fn fetch_workshop_file(app: &AppHandle, path: &str) -> Result, String> { let url = format!("{}/{}", REGISTRY_URL, path); - let resp = CLIENT.get(&url).send().await.map_err(|e| e.to_string())?; + let client = crate::util::build_http_client_from_app(app)?; + let resp = client.get(&url).send().await.map_err(|e| e.to_string())?; if resp.status().is_success() { resp.bytes().await.map(|b| b.to_vec()).map_err(|e| e.to_string()) } else { diff --git a/src/components/common/SkinViewer.tsx b/src/components/common/SkinViewer.tsx index 8a73782..4cd49bf 100644 --- a/src/components/common/SkinViewer.tsx +++ b/src/components/common/SkinViewer.tsx @@ -36,7 +36,7 @@ const SkinViewer = memo(function SkinViewer({ const mountRef = useRef(null); const containerRef = useRef(null); const [focusIndex, setFocusIndex] = useState(0); - const { legacyMode } = useConfig(); + const { legacyMode, animationsEnabled } = useConfig(); const overlaysRef = useRef([]); const capeRef = useRef(null); const capeOrigRef = useRef<{ y: number; rx: number; meshY: number } | null>( @@ -679,10 +679,10 @@ const SkinViewer = memo(function SkinViewer({ return ( diff --git a/src/components/views/HomeView.tsx b/src/components/views/HomeView.tsx index 0406eec..f360b7f 100644 --- a/src/components/views/HomeView.tsx +++ b/src/components/views/HomeView.tsx @@ -14,7 +14,7 @@ import type { Edition } from "../../types/edition"; const HomeView = memo(function HomeView() { const { t } = useTranslation(); const { setActiveView, focusSection, onNavigateToSkin } = useUI(); - const { profile, legacyMode } = useConfig(); + const { profile, legacyMode, animationsEnabled } = useConfig(); const { playPressSound } = useAudio(); const { handleLaunch, @@ -147,10 +147,10 @@ const HomeView = memo(function HomeView() { return ( {buttonsVal.map((btn, i) => ( diff --git a/src/components/views/PckEditorView.tsx b/src/components/views/PckEditorView.tsx index 7208496..0707fc3 100644 --- a/src/components/views/PckEditorView.tsx +++ b/src/components/views/PckEditorView.tsx @@ -257,7 +257,8 @@ export default function PckEditorView() { await TauriService.writeBinaryFile(path, asset.data); showNotification(t("pckEditor.exported", { name: fileName })); } catch (err: unknown) { - if (err !== "CANCELED") showNotification(t("pckEditor.exportFailed"), "error"); + if (err !== "CANCELED") + showNotification(t("pckEditor.exportFailed"), "error"); } }; @@ -589,7 +590,9 @@ export default function PckEditorView() { }} className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`} > - {pck.xmlSupport ? t("pckEditor.enabled") : t("pckEditor.disabled")} + {pck.xmlSupport + ? t("pckEditor.enabled") + : t("pckEditor.disabled")}
diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 19fbca9..0d98544 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -51,6 +51,8 @@ const SettingsView = memo(function SettingsView() { setAndroidRunner: _setAndroidRuntime, androidAudioBackend, setAndroidAudioBackend, + proxy, + setProxy, } = useConfig(); const { currentTrack, skipTrack, tracks, playPressSound, playBackSound } = useAudio(); @@ -80,6 +82,9 @@ const SettingsView = memo(function SettingsView() { const [argsInput, setArgsInput] = useState(""); const [prefixInput, setPrefixInput] = useState(""); const [envVarsInput, setEnvVarsInput] = useState(""); + const [launcherSubMenu, setLauncherSubMenu] = useState< + "display" | "network" | "integrations" | "linux" | "data" | null + >(null); const [showModal, setShowModal] = useState< "args" | "prefix" | "envVars" | null >(null); @@ -287,6 +292,13 @@ const SettingsView = memo(function SettingsView() { small?: boolean; color?: string; textured?: boolean; + } + | { + id: string; + label: string; + type: "textinput"; + value: string; + onChange: (val: string) => void; }; const settingsItems = useMemo(() => { @@ -477,44 +489,122 @@ const SettingsView = memo(function SettingsView() { }, }); } else if (currentSubMenu === "launcher") { - if (!isAndroid) { + if (!launcherSubMenu) { items.push({ - id: "fullscreen", - label: `${t("settings.startInFullscreen")}: ${startFullscreen ? t("common.on") : t("common.off")}`, + id: "cat_display", + label: t("settings.launcherDisplay"), type: "button", - onClick: handleFullscreenToggle, + textured: true, + onClick: () => { + playPressSound(); + setLauncherSubMenu("display"); + setFocusIndex(0); + }, }); items.push({ - id: "rpc", - label: `${t("settings.discordRpc")}: ${rpcEnabled ? t("common.on") : t("common.off")}`, + id: "cat_network", + label: t("settings.launcherNetwork"), type: "button", - onClick: handleRpcToggle, + textured: true, + onClick: () => { + playPressSound(); + setLauncherSubMenu("network"); + setFocusIndex(0); + }, }); - } - items.push({ - id: "skip_intro", - label: `${t("settings.skipIntro")}: ${skipIntro ? t("common.on") : t("common.off")}`, - type: "button", - onClick: handleSkipIntroToggle, - }); - items.push({ - id: "legacy", - label: `${t("settings.legacyMode")}: ${legacyMode ? t("common.on") : t("common.off")}`, - type: "button", - onClick: handleLegacyToggle, - }); - items.push({ - id: "languages", - label: t("settings.languages"), - type: "button", - textured: true, - onClick: () => { - playPressSound(); - setCurrentSubMenu("language"); - setFocusIndex(0); - }, - }); - if (isLinux && !isAndroid) { + items.push({ + id: "cat_integrations", + label: t("settings.launcherMisc"), + type: "button", + textured: true, + onClick: () => { + playPressSound(); + setLauncherSubMenu("integrations"); + setFocusIndex(0); + }, + }); + if (isLinux && !isAndroid) { + items.push({ + id: "cat_linux", + label: t("settings.launcherLinux"), + type: "button", + textured: true, + onClick: () => { + playPressSound(); + setLauncherSubMenu("linux"); + setFocusIndex(0); + }, + }); + } + items.push({ + id: "cat_data", + label: t("settings.launcherData"), + type: "button", + textured: true, + onClick: () => { + playPressSound(); + setLauncherSubMenu("data"); + setFocusIndex(0); + }, + }); + } else if (launcherSubMenu === "display") { + if (!isAndroid) { + items.push({ + id: "fullscreen", + label: `${t("settings.startInFullscreen")}: ${startFullscreen ? t("common.on") : t("common.off")}`, + type: "button", + onClick: handleFullscreenToggle, + }); + } + items.push({ + id: "animations", + label: `${t("settings.animations")}: ${animationsEnabled ? t("common.on") : t("common.off")}`, + type: "button", + onClick: handleAnimationsToggle, + }); + items.push({ + id: "skip_intro", + label: `${t("settings.skipIntro")}: ${skipIntro ? t("common.on") : t("common.off")}`, + type: "button", + onClick: handleSkipIntroToggle, + }); + items.push({ + id: "legacy", + label: `${t("settings.legacyMode")}: ${legacyMode ? t("common.on") : t("common.off")}`, + type: "button", + onClick: handleLegacyToggle, + }); + } else if (launcherSubMenu === "network") { + items.push({ + id: "proxy", + label: t("settings.httpProxy"), + type: "textinput", + value: proxy ?? "", + onChange: (val: string) => { + setProxy(val.trim() || undefined); + }, + }); + } else if (launcherSubMenu === "integrations") { + if (!isAndroid) { + items.push({ + id: "rpc", + label: `${t("settings.discordRpc")}: ${rpcEnabled ? t("common.on") : t("common.off")}`, + type: "button", + onClick: handleRpcToggle, + }); + } + items.push({ + id: "languages", + label: t("settings.languages"), + type: "button", + textured: true, + onClick: () => { + playPressSound(); + setCurrentSubMenu("language"); + setFocusIndex(0); + }, + }); + } else if (launcherSubMenu === "linux") { items.push({ id: "runner", label: `${t("settings.runner")}: ${selectedRunnerName}`, @@ -545,46 +635,46 @@ const SettingsView = memo(function SettingsView() { } }, }); - } - - if (!isAndroid) { + } else if (launcherSubMenu === "data") { + if (!isAndroid) { + items.push({ + id: "export_settings", + label: t("settings.exportSettings"), + type: "button", + textured: true, + onClick: async () => { + playPressSound(); + try { + await TauriService.exportSettings(); + } catch (e) { + if (e !== "CANCELED") console.error(e); + } + }, + }); + items.push({ + id: "import_settings", + label: t("settings.importSettings"), + type: "button", + textured: true, + onClick: async () => { + playPressSound(); + try { + await TauriService.importSettings(); + window.location.reload(); + } catch (e) { + if (e !== "CANCELED") console.error(e); + } + }, + }); + } items.push({ - id: "export_settings", - label: t("settings.exportSettings"), + id: "reset_setup", + label: t("settings.resetSetup"), type: "button", - onClick: async () => { - playPressSound(); - try { - await TauriService.exportSettings(); - } catch (e) { - if (e !== "CANCELED") console.error(e); - } - }, + onClick: handleResetSetup, + color: "orange", }); } - if (!isAndroid) { - items.push({ - id: "import_settings", - label: t("settings.importSettings"), - type: "button", - onClick: async () => { - playPressSound(); - try { - await TauriService.importSettings(); - window.location.reload(); - } catch (e) { - if (e !== "CANCELED") console.error(e); - } - }, - }); - } - items.push({ - id: "reset_setup", - label: t("settings.resetSetup"), - type: "button", - onClick: handleResetSetup, - color: "orange", - }); } else if (currentSubMenu === "language") { const availableLanguages = [ //neo: here goes the list, dont ask why its here @@ -682,6 +772,9 @@ const SettingsView = memo(function SettingsView() { } else if (currentSubMenu === "language") { setCurrentSubMenu("launcher"); setFocusIndex(0); + } else if (currentSubMenu === "launcher" && launcherSubMenu) { + setLauncherSubMenu(null); + setFocusIndex(0); } else { setCurrentSubMenu("main"); setFocusIndex(0); @@ -745,7 +838,10 @@ const SettingsView = memo(function SettingsView() { return; } playBackSound(); - if (currentSubMenu !== "main") { + if (launcherSubMenu) { + setLauncherSubMenu(null); + setFocusIndex(0); + } else if (currentSubMenu !== "main") { setCurrentSubMenu( currentSubMenu === "language" ? "launcher" : "main", ); @@ -758,6 +854,13 @@ const SettingsView = memo(function SettingsView() { const itemCount = settingsItems.length; + if ( + document.activeElement instanceof HTMLInputElement && + document.activeElement.type === "text" + ) { + return; + } + if (e.key === "ArrowDown") { setFocusIndex((prev) => prev === null || prev >= itemCount - 1 ? 0 : prev + 1, @@ -791,6 +894,7 @@ const SettingsView = memo(function SettingsView() { setActiveView, currentSubMenu, showModal, + launcherSubMenu, ]); useEffect(() => { @@ -835,13 +939,14 @@ const SettingsView = memo(function SettingsView() { - {currentSubMenu === "main" ? ( + {currentSubMenu === "main" || + (currentSubMenu === "launcher" && !launcherSubMenu) ? (
{settingsItems.map((item, index) => { if (item.id === "back") return null; @@ -877,6 +982,29 @@ const SettingsView = memo(function SettingsView() { ); } + if (item.type === "textinput") { + return ( +
setFocusIndex(index)} + className="relative w-[480px] flex flex-col cursor-pointer transition-all outline-none shrink-0" + > + + {item.label} + +
+ item.onChange(e.target.value)} + className="mc-textinput w-full h-10 px-3 text-white text-base outline-none font-[var(--font-base)]" + /> +
+
+ ); + } + const isRed = "color" in item && (item as { color: string }).color === "red"; const isSmall = @@ -1015,6 +1143,29 @@ const SettingsView = memo(function SettingsView() { ); } + if (item.type === "textinput") { + return ( +
setFocusIndex(index)} + className="relative w-[600px] flex flex-col cursor-pointer transition-all outline-none shrink-0" + > + + {item.label} + +
+ item.onChange(e.target.value)} + className="mc-textinput w-full h-10 px-3 text-white text-base outline-none font-[var(--font-base)]" + /> +
+
+ ); + } + const isRed = item.type === "button" && item.color === "red"; const isSmall = item.type === "button" && !!item.small; const isTextured = item.type === "button" && !!item.textured; diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index 8c0307b..47ac09e 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -531,9 +531,9 @@ const SkinsView = memo(function SkinsView() { diff --git a/src/context/LauncherContext.tsx b/src/context/LauncherContext.tsx index 13babbb..021b72a 100644 --- a/src/context/LauncherContext.tsx +++ b/src/context/LauncherContext.tsx @@ -112,6 +112,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { configRaw.androidRunner, configRaw.androidAudioBackend, configRaw.goldmapperEnabled, + configRaw.proxy, ], ); @@ -215,6 +216,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { skipIntro: config.skipIntro, instanceLaunchArgs: config.instanceLaunchArgs, goldmapperEnabled: config.goldmapperEnabled, + httpProxy: config.proxy, }).catch(console.error); } }, [ @@ -242,6 +244,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { config.skipIntro, config.instanceLaunchArgs, config.goldmapperEnabled, + config.proxy, ]); useEffect(() => { diff --git a/src/css/index.css b/src/css/index.css index 107002f..84a9595 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -391,3 +391,18 @@ body { border-top: 2px solid #373737; margin: 1.5rem 0; } + +.mc-textinput-outer { + border: 2px solid transparent; + image-rendering: pixelated; +} + +.mc-textinput-outer:focus-within { + border: 2px solid #ffff00; +} + +.mc-textinput { + background-color: #646464 !important; + border: 2px solid #323232 !important; + image-rendering: pixelated; +} diff --git a/src/hooks/useAppConfig.ts b/src/hooks/useAppConfig.ts index 4a2ed64..f69f4c6 100644 --- a/src/hooks/useAppConfig.ts +++ b/src/hooks/useAppConfig.ts @@ -37,6 +37,7 @@ export function useAppConfig() { const [androidAudioBackend, setAndroidAudioBackend] = useLocalStorage<"alsa" | "pulseaudio">("lce-android-audio", "pulseaudio"); const [goldmapperEnabled, setGoldmapperEnabled] = useLocalStorage("lce-goldmapper", true); const [goldmapperMappings, setGoldmapperMappings] = useState(); + const [proxy, setProxy] = useState(); useEffect(() => { TauriService.loadConfig().then((config) => { if (config.username) setUsername(config.username); @@ -65,6 +66,7 @@ export function useAppConfig() { if (config.androidAudioBackend) setAndroidAudioBackend(config.androidAudioBackend); if (config.goldmapperEnabled !== undefined) setGoldmapperEnabled(config.goldmapperEnabled); if (config.goldmapperMappings) setGoldmapperMappings(config.goldmapperMappings); + if (config.httpProxy !== undefined) setProxy(config.httpProxy); setIsLoaded(true); }); }, []); @@ -97,9 +99,10 @@ export function useAppConfig() { androidAudioBackend, goldmapperEnabled, goldmapperMappings, + httpProxy: proxy, }).catch(console.error); } - }, [username, theme, linuxRunner, perfBoost, profile, customEditions, customPaths, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, skipIntro, isLoaded, instanceLaunchArgs, androidRunner, androidAudioBackend, goldmapperEnabled, goldmapperMappings]); + }, [username, theme, linuxRunner, perfBoost, profile, customEditions, customPaths, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, skipIntro, isLoaded, instanceLaunchArgs, androidRunner, androidAudioBackend, goldmapperEnabled, goldmapperMappings, proxy]); return { username, @@ -159,5 +162,7 @@ export function useAppConfig() { setGoldmapperEnabled, goldmapperMappings, setGoldmapperMappings, + proxy, + setProxy, }; } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2d4254b..9c67f02 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -71,6 +71,8 @@ "launchPrefixDesc": "Command that wraps the entire launch (e.g. gamemoderun)", "launchEnvVars": "Launch Env Vars", "launchEnvVarsDesc": "One KEY=VALUE per line", + "httpProxy": "HTTP Proxy", + "httpProxyDesc": "Proxy used for all downloads. Leave blank for direct connection", "startInFullscreen": "Start in Fullscreen", "discordRpc": "Discord RPC", "skipIntro": "Skip Intro", @@ -83,6 +85,11 @@ "importSettings": "Import Settings", "resetSetup": "Reset Setup", "languages": "Languages", + "launcherDisplay": "Display", + "launcherNetwork": "Network", + "launcherMisc": "Misc", + "launcherLinux": "Linux", + "launcherData": "Data", "stopGameButton": "STOP GAME", "noPluginsInstalled": "No plugins installed", "byAuthor": "by {{author}}", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 06e8f80..e949c6c 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -71,6 +71,8 @@ "launchPrefixDesc": "Commande qui entoure le lancement entier (exemple: gamemoderun)", "launchEnvVars": "Lancement Env Vars", "launchEnvVarsDesc": "Une KEY=VALUE par ligne", + "httpProxy": "Proxy HTTP", + "httpProxyDesc": "Proxy utilisé pour tous les téléchargements. Laisser vide pour une connexion directe", "startInFullscreen": "Lancer en plein écran", "discordRpc": "Présence Discord", "skipIntro": "Sauter l'intro", @@ -83,6 +85,11 @@ "importSettings": "Importer les paramètres", "resetSetup": "Réinitialliser", "languages": "Langues", + "launcherDisplay": "Affichage", + "launcherNetwork": "Réseau", + "launcherMisc": "Divers", + "launcherLinux": "Linux", + "launcherData": "Données", "stopGameButton": "Arreter le jeu", "noPluginsInstalled": "Aucun plugins installé", "byAuthor": "par {{author}}", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index e3b0495..1fe6777 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -71,6 +71,8 @@ "launchPrefixDesc": "Команда для обёртки запуска (нап. gamemoderun)", "launchEnvVars": "Переменные окружения", "launchEnvVarsDesc": "По одной паре КЛЮЧ=ЗНАЧЕНИЕ на строку", + "httpProxy": "HTTP-прокси", + "httpProxyDesc": "Прокси для всех загрузок. Оставьте пустым для прямого подключения", "startInFullscreen": "Запустить на полный экран", "discordRpc": "Интеграция Discord", "skipIntro": "Пропустить заставку", @@ -83,6 +85,11 @@ "importSettings": "Импорт настроек", "resetSetup": "Сбросить настройки", "languages": "Языки", + "launcherDisplay": "Дисплей", + "launcherNetwork": "Сеть", + "launcherMisc": "Прочее", + "launcherLinux": "Linux", + "launcherData": "Данные", "stopGameButton": "ЗАКРЫТЬ ИГРУ", "noPluginsInstalled": "Плагины не установлены", "byAuthor": "от {{author}}", diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index 5299353..e19e53c 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -61,6 +61,7 @@ export interface AppConfig { androidAudioBackend?: "alsa" | "pulseaudio"; goldmapperEnabled?: boolean; goldmapperMappings?: GoldMapperMapping[]; + httpProxy?: string; } export interface ThemePalette {