mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 17:11:00 +00:00
feat: more ui tweaks
This commit is contained in:
Generated
+13
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<Vec<GitEntry>, 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<Vec<String>, 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))?;
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<String>,
|
||||
headers: std::collections::HashMap<String, String>,
|
||||
) -> Result<HttpResponse, String> {
|
||||
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),
|
||||
|
||||
@@ -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<St
|
||||
|
||||
let filename = format!("{}.{}", id, file_ext);
|
||||
let dest_path = logos_dir.join(&filename);
|
||||
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 logo: {}", response.status()));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ async fn download_and_assemble(
|
||||
let mut downloaded: Vec<PathBuf> = 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()));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ pub struct AppConfig {
|
||||
pub android_audio_backend: Option<String>,
|
||||
pub goldmapper_enabled: Option<bool>,
|
||||
pub goldmapper_mappings: Option<Vec<GoldMapperMapping>>,
|
||||
pub http_proxy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
|
||||
@@ -44,6 +44,24 @@ pub fn copy_dir_all(src: impl AsRef<std::path::Path>, dst: impl AsRef<std::path:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_http_client(proxy: Option<&str>) -> Result<reqwest::Client, String> {
|
||||
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<reqwest::Client, String> {
|
||||
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();
|
||||
|
||||
@@ -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<reqwest::Client> = Lazy::new(|| reqwest::Client::new());
|
||||
pub struct Guard {
|
||||
cancel: Option<CancellationToken>,
|
||||
}
|
||||
@@ -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<Vec<u8>, String> {
|
||||
async fn fetch_workshop_file(app: &AppHandle, path: &str) -> Result<Vec<u8>, 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 {
|
||||
|
||||
@@ -36,7 +36,7 @@ const SkinViewer = memo(function SkinViewer({
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [focusIndex, setFocusIndex] = useState(0);
|
||||
const { legacyMode } = useConfig();
|
||||
const { legacyMode, animationsEnabled } = useConfig();
|
||||
const overlaysRef = useRef<THREE.Mesh[]>([]);
|
||||
const capeRef = useRef<THREE.Group | null>(null);
|
||||
const capeOrigRef = useRef<{ y: number; rx: number; meshY: number } | null>(
|
||||
@@ -679,10 +679,10 @@ const SkinViewer = memo(function SkinViewer({
|
||||
return (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
|
||||
initial={{ opacity: animationsEnabled ? 0 : 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: animationsEnabled ? 0 : 1 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className={`absolute ${legacyMode ? "left-[calc(50vw-340px)]" : "left-16"} ${legacyMode ? "top-1/2" : "top-[40%]"} -translate-y-1/2 flex flex-col items-center gap-1 outline-none z-10`}
|
||||
style={style}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
tabIndex={-1}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
initial={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? 10 : 0 }}
|
||||
animate={{ opacity: isFocusedSection ? 1 : 0.5, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
|
||||
exit={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? 10 : 0 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="relative w-full max-w-[540px] flex flex-col space-y-3 outline-none"
|
||||
>
|
||||
{buttonsVal.map((btn, i) => (
|
||||
|
||||
@@ -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")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
|
||||
@@ -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<SettingsItem[]>(() => {
|
||||
@@ -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() {
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
tabIndex={-1}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: animationsEnabled ? 0 : 1, scale: animationsEnabled ? 0.95 : 1 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
exit={{ opacity: animationsEnabled ? 0 : 1, scale: animationsEnabled ? 0.95 : 1 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-5xl outline-none"
|
||||
>
|
||||
{currentSubMenu === "main" ? (
|
||||
{currentSubMenu === "main" ||
|
||||
(currentSubMenu === "launcher" && !launcherSubMenu) ? (
|
||||
<div className="w-full max-w-[680px] space-y-2 mb-4 p-6 flex flex-col items-center overflow-y-auto max-h-[55vh] settings-scrollbar">
|
||||
{settingsItems.map((item, index) => {
|
||||
if (item.id === "back") return null;
|
||||
@@ -877,6 +982,29 @@ const SettingsView = memo(function SettingsView() {
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "textinput") {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
data-index={index}
|
||||
onMouseEnter={() => setFocusIndex(index)}
|
||||
className="relative w-[480px] flex flex-col cursor-pointer transition-all outline-none shrink-0"
|
||||
>
|
||||
<span className="text-black text-base font-[var(--font-base)] mb-1">
|
||||
{item.label}
|
||||
</span>
|
||||
<div className="mc-textinput-outer">
|
||||
<input
|
||||
type="text"
|
||||
value={item.value}
|
||||
onChange={(e) => item.onChange(e.target.value)}
|
||||
className="mc-textinput w-full h-10 px-3 text-white text-base outline-none font-[var(--font-base)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
key={item.id}
|
||||
data-index={index}
|
||||
onMouseEnter={() => setFocusIndex(index)}
|
||||
className="relative w-[600px] flex flex-col cursor-pointer transition-all outline-none shrink-0"
|
||||
>
|
||||
<span className="text-black text-base font-[var(--font-base)] mb-1">
|
||||
{item.label}
|
||||
</span>
|
||||
<div className="mc-textinput-outer">
|
||||
<input
|
||||
type="text"
|
||||
value={item.value}
|
||||
onChange={(e) => item.onChange(e.target.value)}
|
||||
className="mc-textinput w-full h-10 px-3 text-white text-base outline-none font-[var(--font-base)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isRed = item.type === "button" && item.color === "red";
|
||||
const isSmall = item.type === "button" && !!item.small;
|
||||
const isTextured = item.type === "button" && !!item.textured;
|
||||
|
||||
@@ -531,9 +531,9 @@ const SkinsView = memo(function SkinsView() {
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
tabIndex={-1}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-3xl h-full outline-none"
|
||||
>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<GoldMapperMapping[] | undefined>();
|
||||
const [proxy, setProxy] = useState<string | undefined>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface AppConfig {
|
||||
androidAudioBackend?: "alsa" | "pulseaudio";
|
||||
goldmapperEnabled?: boolean;
|
||||
goldmapperMappings?: GoldMapperMapping[];
|
||||
httpProxy?: string;
|
||||
}
|
||||
|
||||
export interface ThemePalette {
|
||||
|
||||
Reference in New Issue
Block a user