diff --git a/.gitignore b/.gitignore index 3592807..9b75ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ Release/ *.o *.lib *.a -*.dll *.so *.dylib *.exe diff --git a/RLIdentity.dll b/RLIdentity.dll new file mode 100644 index 0000000..c3b05b1 Binary files /dev/null and b/RLIdentity.dll differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 284ffde..3b92d8c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3457,9 +3457,11 @@ name = "rlidentitygui" version = "2.0.0" dependencies = [ "dirs 5.0.1", + "hex", "reqwest 0.12.28", "serde", "serde_json", + "sha2", "sysinfo", "tauri", "tauri-build", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d4edebd..78c7bca 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,6 @@ dirs = "5" tauri-plugin-updater = "2.10.0" tauri-plugin-process = "2.3.1" sha2 = "0.10" - +hex = "0.4" [target.'cfg(target_os = "windows")'.dependencies] window-vibrancy = "0.5.2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index afaa4bb..6f3f535 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,14 @@ -use reqwest; -use tauri::{Manager, WebviewWindow}; -use window_vibrancy::apply_acrylic; +use reqwest::Client; use serde::Serialize; -use std::fs; -use sysinfo::System; +use std::path::{Path, PathBuf}; use std::process::Command; -use std::path::Path; +use tokio::fs; +use tauri::{Manager, State, WebviewWindow}; +use window_vibrancy::apply_acrylic; +use sysinfo::System; +use sha2::{Sha256, Digest}; + +// --- types --- #[derive(Serialize)] pub struct Status { @@ -29,86 +32,66 @@ pub struct KeyValidationResponse { pub user: Option, } -#[tauri::command] -fn minimize_to_tray(window: WebviewWindow) { - let _ = window.hide(); +// global state for optimization +struct AppState { + client: Client, + app_data: PathBuf, } -#[tauri::command] -fn get_hwid() -> String { - // Simple HWID using Windows UUID - let output = Command::new("wmic") - .args(["csproduct", "get", "uuid"]) - .output() - .ok(); - - if let Some(out) = output { - let s = String::from_utf8_lossy(&out.stdout); - let lines: Vec<&str> = s.lines().collect(); - if lines.len() >= 2 { - return lines[1].trim().to_string(); - } - } - "UNKNOWN-HWID".to_string() -} +// --- helpers --- -fn get_last_epic_id() -> String { - if let Some(mut path) = dirs::data_dir() { - path.push("RLidentity"); - path.push("last_epic_id.txt"); - - if let Ok(content) = fs::read_to_string(path) { - let trimmed = content.trim(); - if trimmed.len() == 32 { - return trimmed.to_string(); - } +async fn get_last_epic_id(base_path: &Path) -> String { + let path = base_path.join("last_epic_id.txt"); + if let Ok(content) = fs::read_to_string(path).await { + let trimmed = content.trim(); + if trimmed.len() == 32 { + return trimmed.to_string(); } } "".to_string() } -#[tauri::command] -async fn validate_key(key: String, hwid: String) -> Result { - let epic_id = get_last_epic_id(); - // Added epicId query parameter to the URL - let url = format!( - "https://api.rlidentity.me/keys/{}?hwid={}&epicId={}", - key, hwid, epic_id - ); - - let client = reqwest::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .map_err(|e| format!("Client Error: {}", e))?; - - println!("[LOG] Connecting to: {}", url); - println!("[LOG] Sending Epic ID: {}", epic_id); +// --- commands --- - let res = client.get(&url) +#[tauri::command] +fn get_hwid() -> String { + // direct registry query for speed + let output = Command::new("reg") + .args(["query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"]) + .output(); + + if let Ok(out) = output { + let s = String::from_utf8_lossy(&out.stdout); + if let Some(guid) = s.split_whitespace().last() { + if guid.len() == 36 && guid.contains('-') { + return guid.to_string(); + } + } + } + "UNKNOWN-HWID".to_string() +} + + + +#[tauri::command] +async fn validate_key( + key: String, + hwid: String, + state: State<'_, AppState> +) -> Result { + let epic_id = get_last_epic_id(&state.app_data).await; + let url = format!("https://api.rlidentity.me/keys/{}?hwid={}&epicId={}", key, hwid, epic_id); + + let res = state.client.get(&url) .send() .await - .map_err(|e| { - let err_msg = format!("Network Error: {}. Is the server on 443?", e); - println!("[ERROR] {}", err_msg); - err_msg - })?; + .map_err(|e| format!("network error: {}", e))?; - println!("[LOG] HTTP Status: {}", res.status()); - - let json: serde_json::Value = res.json().await.map_err(|e| { - let err_msg = format!("JSON Parse Error: {}", e); - println!("[ERROR] {}", err_msg); - err_msg - })?; + let json: serde_json::Value = res.json().await.map_err(|e| format!("json error: {}", e))?; - println!("[LOG] Server Payload: {:?}", json); - let status = json.get("status").and_then(|v| v.as_str()).unwrap_or("unknown").to_string(); - let user = json.get("user").map(|u| UserInfo { - userId: u.get("userId").and_then(|v| { - v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string())) - }), + userId: u.get("userId").and_then(|v| v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string()))), discordId: u.get("discordId").and_then(|v| v.as_str()).map(|s| s.to_string()), epicId: u.get("epicId").and_then(|v| v.as_str()).map(|s| s.to_string()), username: u.get("username").and_then(|v| v.as_str()).map(|s| s.to_string()), @@ -120,18 +103,70 @@ async fn validate_key(key: String, hwid: String) -> Result Result<(), String> { - let mut path = dirs::data_dir().ok_or("Could not find AppData")?; - path.push("RLidentity"); - fs::create_dir_all(&path).map_err(|e| e.to_string())?; - path.push("config.json"); +async fn inject_dll(state: State<'_, AppState>) -> Result { + let injector_path = state.app_data.join("injector.exe"); + let dll_path = state.app_data.join("RLIdentity.dll"); + + let mut s = System::new_all(); + s.refresh_processes(); + if s.processes_by_exact_name("RocketLeague.exe").next().is_none() { + return Err("rocket league is not running".into()); + } - let json = serde_json::json!({ - "spoofedName": name, - "platform": platform - }); + if !injector_path.exists() || !dll_path.exists() { + return Err("files missing, wait for update".into()); + } - fs::write(path, serde_json::to_string_pretty(&json).unwrap()).map_err(|e| e.to_string())?; + let output = Command::new(injector_path) + .arg("RocketLeague.exe") + .arg(dll_path) + .output() + .map_err(|e| format!("exec failed: {}", e))?; + + if output.status.success() { + Ok("injected".into()) + } else { + Err("injection failed".into()) + } +} + +#[tauri::command] +async fn download_assets(state: State<'_, AppState>) -> Result<(), String> { + fs::create_dir_all(&state.app_data).await.map_err(|e| e.to_string())?; + + // in a real scenario, you'd fetch these hashes from your api first + let assets = [ + ( + "injector.exe", + "https://git.rlidentity.me/bits/RLidentity/src/branch/dll/injector.exe", + "EXPECTED_SHA256_HASH_HERE" + ), + ( + "RLIdentity.dll", + "https://git.rlidentity.me/.../RLIdentity.dll", + "EXPECTED_SHA256_HASH_HERE" + ), + ]; + + for (name, url, expected_hash) in assets { + let file_path = state.app_data.join(name); + + // download + let res = state.client.get(url).send().await.map_err(|e| e.to_string())?; + let bytes = res.bytes().await.map_err(|e| e.to_string())?; + + // verify integrity (signature check) + let mut hasher = Sha256::new(); + hasher.update(&bytes); + let actual_hash = hex::encode(hasher.finalize()); + + if actual_hash != expected_hash { + return Err(format!("integrity check failed for {}: hash mismatch", name)); + } + + // only write if the "signature" (hash) is correct + fs::write(file_path, bytes).await.map_err(|e| e.to_string())?; + } Ok(()) } @@ -144,74 +179,26 @@ async fn check_status() -> Status { } #[tauri::command] -async fn download_assets() -> Result<(), String> { - let mut path = dirs::data_dir().ok_or("Could not find AppData")?; - path.push("RLidentity"); - fs::create_dir_all(&path).map_err(|e| e.to_string())?; - - let client = reqwest::Client::new(); - let assets = [ - ("injector.exe", "https://git.rlidentity.me/bits/RLidentity/raw/branch/dll/injector.exe"), - ("RLIdentity.dll", "https://git.rlidentity.me/bits/RLidentity/raw/branch/dll/RLIdentity.dll"), - ]; - - for (name, url) in assets { - let mut file_path = path.clone(); - file_path.push(name); - - let response = client.get(url).send().await.map_err(|e| e.to_string())?; - let bytes = response.bytes().await.map_err(|e| e.to_string())?; - fs::write(file_path, bytes).map_err(|e| e.to_string())?; - } - Ok(()) +fn minimize_to_tray(window: WebviewWindow) { + let _ = window.hide(); } -#[tauri::command] -async fn inject_dll(_discordId: Option) -> Result { - let mut base_path = dirs::data_dir().ok_or("Could not find AppData")?; - base_path.push("RLidentity"); - - let injector_path = base_path.join("injector.exe"); - let dll_path = base_path.join("RLIdentity.dll"); - - let mut s = System::new_all(); - s.refresh_processes(); - if s.processes_by_exact_name("RocketLeague.exe").next().is_none() { - return Err("Rocket League is not running!".into()); - } - - if !injector_path.exists() || !dll_path.exists() { - return Err("Required files missing. Please wait for update to finish.".into()); - } - - // Run the injector and capture FULL output - let output = Command::new(injector_path) - .arg("RocketLeague.exe") - .arg(dll_path) - .output() - .map_err(|e| format!("Execution failed: {}", e))?; - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - let full_log = format!("STDOUT:\n{}\n\nSTDERR:\n{}", stdout, stderr); - println!("[LOG] Injector results:\n{}", full_log); - - if output.status.success() { - Ok(format!("Successfully injected!\n\n{}", stdout)) - } else { - Err(format!("Injection failed!\n\n{}", full_log)) - } -} +// --- main --- pub fn run() { tauri::Builder::default() + .manage(AppState { + client: Client::builder() + .danger_accept_invalid_certs(false) + .build() + .unwrap(), + app_data: dirs::data_dir().unwrap().join("RLidentity"), + }) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .invoke_handler(tauri::generate_handler![ minimize_to_tray, - save_config, inject_dll, validate_key, check_status, @@ -219,41 +206,31 @@ pub fn run() { download_assets ]) .setup(|app| { - let icon_bytes = include_bytes!("../icons/32x32.png"); - let icon = tauri::image::Image::from_bytes(icon_bytes)?; - let window = app.get_webview_window("main").unwrap(); - window.set_icon(icon.clone())?; - + #[cfg(target_os = "windows")] apply_acrylic(&window, Some((18, 18, 18, 125))).ok(); let handle = app.handle().clone(); let tray_menu = tauri::menu::Menu::with_items(app, &[ - &tauri::menu::MenuItem::with_id(app, "tray_quit", "Quit", true, None::<&str>)?, + &tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?, ])?; - tauri::tray::TrayIconBuilder::new() - .icon(icon) + let _ = tauri::tray::TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) .menu(&tray_menu) .on_menu_event(move |_app, event| { - if event.id().as_ref() == "tray_quit" { handle.exit(0); } + if event.id().as_ref() == "quit" { handle.exit(0); } }) .on_tray_icon_event(|tray, event| { - if let tauri::tray::TrayIconEvent::Click { - button: tauri::tray::MouseButton::Left, - .. - } = event { - let app = tray.app_handle(); - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - } + if let tauri::tray::TrayIconEvent::Click { button: tauri::tray::MouseButton::Left, .. } = event { + let _ = tray.app_handle().get_webview_window("main").unwrap().show(); } }) .build(app)?; + Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} +} \ No newline at end of file diff --git a/src/App.css b/src/App.css index e0148e8..3754eb0 100644 --- a/src/App.css +++ b/src/App.css @@ -1,23 +1,190 @@ -/* --- Premium dark + neon purple glassmorphism theme (RLidentity) --- */ +/* ───────────────────────────────────────────────────────────────────────────── + RLidentity — Theme system + UI + Accent colors use --accent-rgb and --accent-light-rgb so every rgba() + call picks up the active theme automatically. +───────────────────────────────────────────────────────────────────────────── */ -:root { +/* ── Phantom (default) — deep purple, mysterious ── */ +:root, +[data-theme="phantom"] { color-scheme: dark; + + --accent: #a855f7; + --accent-mid: #7c3aed; + --accent-light: #c084fc; + --accent-rgb: 168, 85, 247; + --accent-mid-rgb: 124, 58, 237; + --accent-light-rgb: 192, 132, 252; + --bg0: #07060b; --bg1: #0b0620; - --purple0: #a855f7; - --purple1: #7c3aed; - --purple2: #c084fc; + --red0: #ff3333; --red1: #cc0000; - --glass: rgba(168, 85, 247, 0.10); - --glass-2: rgba(168, 85, 247, 0.14); - --glass-red: rgba(255, 51, 51, 0.15); - --glass-red-2: rgba(255, 51, 51, 0.25); - --stroke: rgba(192, 132, 252, 0.28); - --stroke-2: rgba(168, 85, 247, 0.38); - --stroke-red: rgba(255, 85, 85, 0.45); - --text: rgba(245, 243, 255, 0.92); - --muted: rgba(245, 243, 255, 0.62); + + --glass: rgba(var(--accent-rgb), 0.10); + --glass-2: rgba(var(--accent-rgb), 0.14); + --stroke: rgba(var(--accent-light-rgb), 0.28); + --stroke-2: rgba(var(--accent-rgb), 0.38); + --text: rgba(245, 243, 255, 0.92); + --muted: rgba(245, 243, 255, 0.62); +} + +/* ── Glacier — icy cold cyan on pitch black ── */ +[data-theme="glacier"] { + --accent: #38bdf8; + --accent-mid: #0284c7; + --accent-light: #bae6fd; + --accent-rgb: 56, 189, 248; + --accent-mid-rgb: 2, 132, 199; + --accent-light-rgb: 186, 230, 253; + + --bg0: #020c14; + --bg1: #030f1c; + + --red0: #ff3333; + --red1: #cc0000; + + --glass: rgba(var(--accent-rgb), 0.08); + --glass-2: rgba(var(--accent-rgb), 0.12); + --stroke: rgba(var(--accent-light-rgb), 0.22); + --stroke-2: rgba(var(--accent-rgb), 0.35); + --text: rgba(224, 242, 254, 0.93); + --muted: rgba(186, 230, 253, 0.55); +} + +/* ── Inferno — molten orange, aggressive heat ── */ +[data-theme="inferno"] { + --accent: #f97316; + --accent-mid: #c2410c; + --accent-light: #fdba74; + --accent-rgb: 249, 115, 22; + --accent-mid-rgb: 194, 65, 12; + --accent-light-rgb: 253, 186, 116; + + --bg0: #0f0602; + --bg1: #190800; + + --red0: #ff3333; + --red1: #cc0000; + + --glass: rgba(var(--accent-rgb), 0.09); + --glass-2: rgba(var(--accent-rgb), 0.13); + --stroke: rgba(var(--accent-light-rgb), 0.25); + --stroke-2: rgba(var(--accent-rgb), 0.40); + --text: rgba(255, 247, 237, 0.93); + --muted: rgba(253, 186, 116, 0.55); +} + +/* ── Matrix — terminal acid green on near-black ── */ +[data-theme="matrix"] { + --accent: #00ff41; + --accent-mid: #00b32d; + --accent-light: #69ff87; + --accent-rgb: 0, 255, 65; + --accent-mid-rgb: 0, 179, 45; + --accent-light-rgb: 105, 255, 135; + + --bg0: #010a03; + --bg1: #010d04; + + --red0: #ff3333; + --red1: #cc0000; + + --glass: rgba(var(--accent-rgb), 0.06); + --glass-2: rgba(var(--accent-rgb), 0.09); + --stroke: rgba(var(--accent-rgb), 0.20); + --stroke-2: rgba(var(--accent-rgb), 0.32); + --text: rgba(220, 255, 225, 0.92); + --muted: rgba(105, 255, 135, 0.50); +} + +/* ── Synthwave — hot pink on deep purple-black ── */ +[data-theme="synthwave"] { + --accent: #f72585; + --accent-mid: #7209b7; + --accent-light: #ff6eb4; + --accent-rgb: 247, 37, 133; + --accent-mid-rgb: 114, 9, 183; + --accent-light-rgb: 255, 110, 180; + + --bg0: #0d0614; + --bg1: #120519; + + --red0: #ff3333; + --red1: #cc0000; + + --glass: rgba(var(--accent-rgb), 0.09); + --glass-2: rgba(var(--accent-rgb), 0.13); + --stroke: rgba(var(--accent-light-rgb), 0.25); + --stroke-2: rgba(var(--accent-rgb), 0.38); + --text: rgba(255, 236, 245, 0.93); + --muted: rgba(255, 110, 180, 0.55); +} + +/* ── Eclipse — rich gold, luxury dark ── */ +[data-theme="eclipse"] { + --accent: #fbbf24; + --accent-mid: #92400e; + --accent-light: #fde68a; + --accent-rgb: 251, 191, 36; + --accent-mid-rgb: 146, 64, 14; + --accent-light-rgb: 253, 230, 138; + + --bg0: #0c0900; + --bg1: #120c00; + + --red0: #ff3333; + --red1: #cc0000; + + --glass: rgba(var(--accent-rgb), 0.07); + --glass-2: rgba(var(--accent-rgb), 0.11); + --stroke: rgba(var(--accent-light-rgb), 0.22); + --stroke-2: rgba(var(--accent-rgb), 0.35); + --text: rgba(255, 251, 235, 0.93); + --muted: rgba(253, 230, 138, 0.55); +} + +/* Matrix gets a stronger, tighter glow — green on black pops differently */ +[data-theme="matrix"] .neon-ring::before { + opacity: 1; + filter: blur(6px); +} + +[data-theme="matrix"] .btn-primary { + color: #000; + font-family: "Courier New", Courier, monospace; + letter-spacing: 0.08em; +} + +[data-theme="matrix"] .neon-text, +[data-theme="matrix"] .neon-text-soft { + font-family: "Courier New", Courier, monospace; + letter-spacing: 0.04em; +} + +/* Synthwave gets a dual-color glow on the ring */ +[data-theme="synthwave"] .neon-ring::before { + background: + radial-gradient(120px 80px at 15% 10%, rgba(247, 37, 133, 0.45), transparent 55%), + radial-gradient(100px 70px at 85% 90%, rgba(114, 9, 183, 0.35), transparent 55%); +} + +/* Inferno gets a warm bottom glow */ +[data-theme="inferno"] .neon-ring::before { + background: radial-gradient(160px 100px at 80% 90%, rgba(249, 115, 22, 0.40), transparent 60%); +} + +/* Eclipse gets a golden shimmer at the top */ +[data-theme="eclipse"] .neon-ring::before { + background: radial-gradient(180px 80px at 50% 0%, rgba(251, 191, 36, 0.38), transparent 65%); + filter: blur(8px); +} + +*, +*::before, +*::after { + box-sizing: border-box; } html, @@ -29,132 +196,58 @@ body, body { margin: 0; font-family: - ui-sans-serif, - system-ui, - -apple-system, - "Segoe UI", - Roboto, - Helvetica, - Arial, - "Apple Color Emoji", - "Segoe UI Emoji"; + ui-sans-serif, system-ui, -apple-system, + "Segoe UI", Roboto, Helvetica, Arial, + "Apple Color Emoji", "Segoe UI Emoji"; letter-spacing: 0.01em; - background: radial-gradient(1200px 700px at 50% 20%, rgba(168, 85, 247, 0.18), transparent 55%), - radial-gradient(900px 600px at 20% 80%, rgba(124, 58, 237, 0.14), transparent 50%), - linear-gradient(180deg, var(--bg0), var(--bg1)); + background: + radial-gradient(1200px 700px at 50% 20%, rgba(var(--accent-rgb), 0.18), transparent 55%), + radial-gradient(900px 600px at 20% 80%, rgba(var(--accent-mid-rgb), 0.14), transparent 50%), + linear-gradient(180deg, var(--bg0), var(--bg1)); color: var(--text); overflow: hidden; + transition: background 0.4s ease; } body.revoked-bg { - background: radial-gradient(1200px 700px at 50% 20%, rgba(255, 51, 51, 0.25), transparent 55%), + background: + radial-gradient(1200px 700px at 50% 20%, rgba(255, 51, 51, 0.25), transparent 55%), radial-gradient(900px 600px at 20% 80%, rgba(204, 0, 0, 0.20), transparent 50%), linear-gradient(180deg, #1a0505, #0a0000); } -.setting-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 10px 6px; -} - -.setting-text { - display: grid; - gap: 4px; - min-width: 0; -} - -.setting-title { - font-weight: 800; - color: rgba(245, 243, 255, 0.88); - letter-spacing: 0.01em; -} - -.setting-sub { - font-size: 12px; - color: rgba(245, 243, 255, 0.62); - line-height: 1.4; -} - -.switch { - position: relative; - width: 46px; - height: 28px; - flex: 0 0 auto; -} - -.switch input { - position: absolute; - inset: 0; - opacity: 0; - cursor: pointer; -} - -.switch-ui { - position: absolute; - inset: 0; - border-radius: 999px; - background: rgba(10, 8, 18, 0.35); - border: 1px solid rgba(192, 132, 252, 0.28); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); -} - -.switch-ui::after { - content: ""; - position: absolute; - top: 50%; - left: 4px; - width: 20px; - height: 20px; - transform: translateY(-50%); - border-radius: 999px; - background: linear-gradient(135deg, rgba(245, 243, 255, 0.9), rgba(192, 132, 252, 0.55)); - box-shadow: - 0 8px 18px rgba(0, 0, 0, 0.35), - 0 0 14px rgba(168, 85, 247, 0.18); -} - -.switch input:checked + .switch-ui { - background: rgba(168, 85, 247, 0.18); - border-color: rgba(192, 132, 252, 0.55); - box-shadow: - 0 0 0 4px rgba(168, 85, 247, 0.10), - 0 0 18px rgba(168, 85, 247, 0.14), - inset 0 1px 0 rgba(255, 255, 255, 0.05); -} - -.switch input:checked + .switch-ui::after { - left: 22px; -} - +/* ───────────────────────────────────────────────────────────────────────────── + Aurora background layer +───────────────────────────────────────────────────────────────────────────── */ .bg-aurora { position: fixed; inset: -20%; background: - radial-gradient(600px 400px at 30% 30%, rgba(168, 85, 247, 0.12), transparent 60%), - radial-gradient(700px 500px at 70% 60%, rgba(124, 58, 237, 0.10), transparent 60%), - radial-gradient(900px 700px at 50% 50%, rgba(192, 132, 252, 0.06), transparent 65%); + radial-gradient(600px 400px at 30% 30%, rgba(var(--accent-rgb), 0.12), transparent 60%), + radial-gradient(700px 500px at 70% 60%, rgba(var(--accent-mid-rgb), 0.10), transparent 60%), + radial-gradient(900px 700px at 50% 50%, rgba(var(--accent-light-rgb), 0.06), transparent 65%); filter: blur(12px); opacity: 0.9; animation: float 10s ease-in-out infinite; pointer-events: none; + transition: background 0.4s ease; } .revoked-aurora { - background: - radial-gradient(600px 400px at 30% 30%, rgba(255, 51, 51, 0.20), transparent 60%), - radial-gradient(700px 500px at 70% 60%, rgba(204, 0, 0, 0.15), transparent 60%), - radial-gradient(900px 700px at 50% 50%, rgba(255, 85, 85, 0.12), transparent 65%); + background: + radial-gradient(600px 400px at 30% 30%, rgba(255, 51, 51, 0.20), transparent 60%), + radial-gradient(700px 500px at 70% 60%, rgba(204, 0, 0, 0.15), transparent 60%), + radial-gradient(900px 700px at 50% 50%, rgba(255, 85, 85, 0.12), transparent 65%); } @keyframes float { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); } - 50% { transform: translate3d(0, -10px, 0) scale(1.02); } + 50% { transform: translate3d(0, -10px, 0) scale(1.02); } } -/* --- App Shell --- */ +/* ───────────────────────────────────────────────────────────────────────────── + App shell +───────────────────────────────────────────────────────────────────────────── */ .app-shell { height: 100%; display: flex; @@ -164,12 +257,12 @@ body.revoked-bg { padding: 20px; } -/* --- Titlebar --- */ +/* ───────────────────────────────────────────────────────────────────────────── + Titlebar +───────────────────────────────────────────────────────────────────────────── */ .window-titlebar { position: fixed; - top: 0; - left: 0; - right: 0; + top: 0; left: 0; right: 0; height: 48px; display: flex; align-items: center; @@ -193,14 +286,31 @@ body.revoked-bg { width: 24px; height: 24px; object-fit: contain; - filter: drop-shadow(0 0 8px rgba(168, 85, 247, 0.25)); + filter: drop-shadow(0 0 8px rgba(var(--accent-rgb), 0.35)); +} + +.titlebar-text { + display: grid; + gap: 2px; +} + +.app-name { + font-weight: 900; + font-size: 13px; + letter-spacing: 0.05em; +} + +.app-slogan { + font-size: 11px; + color: var(--muted); + line-height: 1.3; } .titlebar-app-name { font-weight: 900; font-size: 14px; letter-spacing: 0.05em; - color: rgba(245, 243, 255, 0.92); + color: var(--text); } .titlebar-controls-wrap { @@ -219,7 +329,7 @@ body.revoked-bg { .tb-action { appearance: none; - border: 1px solid rgba(192, 132, 252, 0.22); + border: 1px solid rgba(var(--accent-light-rgb), 0.22); background: rgba(10, 8, 18, 0.22); color: rgba(245, 243, 255, 0.82); border-radius: 10px; @@ -227,18 +337,17 @@ body.revoked-bg { cursor: pointer; font-weight: 800; font-size: 12px; + transition: border-color 0.2s, background 0.2s; } -.tb-divider { - width: 1px; - height: 18px; - background: rgba(255, 255, 255, 0.10); - margin: 0 4px; +.tb-action:hover { + border-color: rgba(var(--accent-light-rgb), 0.45); + background: rgba(var(--accent-rgb), 0.12); } .win-btn { appearance: none; - border: 1px solid rgba(192, 132, 252, 0.20); + border: 1px solid rgba(var(--accent-light-rgb), 0.20); background: rgba(10, 8, 18, 0.28); color: rgba(245, 243, 255, 0.82); border-radius: 10px; @@ -246,9 +355,16 @@ body.revoked-bg { cursor: pointer; line-height: 1; font-weight: 800; + transition: background 0.2s; } -/* --- Welcome Section --- */ +.win-btn:hover { + background: rgba(255, 255, 255, 0.06); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Welcome section +───────────────────────────────────────────────────────────────────────────── */ .welcome-section { text-align: center; margin-bottom: 24px; @@ -266,16 +382,18 @@ body.revoked-bg { display: inline-block; margin-top: 8px; padding: 4px 12px; - background: rgba(168, 85, 247, 0.15); - border: 1px solid rgba(192, 132, 252, 0.3); + background: rgba(var(--accent-rgb), 0.15); + border: 1px solid rgba(var(--accent-light-rgb), 0.30); border-radius: 99px; font-size: 11px; font-weight: 700; - color: var(--purple2); + color: var(--accent-light); letter-spacing: 0.05em; } -/* --- Main Card --- */ +/* ───────────────────────────────────────────────────────────────────────────── + Panel & cards +───────────────────────────────────────────────────────────────────────────── */ .panel-wrap { width: min(520px, 94vw); position: relative; @@ -287,7 +405,17 @@ body.revoked-bg { border: 1px solid rgba(255, 255, 255, 0.08); backdrop-filter: blur(18px); box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55); - overflow: hidden; + /* overflow: visible so dropdowns inside modals aren't clipped */ + overflow: visible; +} + +/* Clip the card bg without clipping absolute children */ +.glass-card::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 22px; + pointer-events: none; } .neon-ring { position: relative; } @@ -296,112 +424,114 @@ body.revoked-bg { position: absolute; inset: -2px; border-radius: 24px; - background: radial-gradient(140px 90px at 20% 10%, rgba(192, 132, 252, 0.35), transparent 60%); + background: radial-gradient(140px 90px at 20% 10%, rgba(var(--accent-light-rgb), 0.35), transparent 60%); filter: blur(10px); opacity: 0.85; z-index: -1; + transition: background 0.4s ease; } -.card-header { padding: 26px 26px 10px; text-align: center; } -.headline { margin: 0; font-weight: 800; font-size: 26px; } -.neon-text { color: #fff; text-shadow: 0 0 10px rgba(168, 85, 247, 0.4); } +.card-header { + padding: 26px 26px 10px; + text-align: center; +} -.form-stack { padding: 10px 26px 20px; display: grid; gap: 16px; } -.label { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; display: block; } +.headline { + margin: 0; + font-weight: 800; + font-size: 26px; +} + +.neon-text { + color: #fff; + text-shadow: 0 0 12px rgba(var(--accent-rgb), 0.50); + transition: text-shadow 0.4s ease; +} + +.neon-text-soft { + text-shadow: 0 0 8px rgba(var(--accent-rgb), 0.40); + transition: text-shadow 0.4s ease; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Form elements +───────────────────────────────────────────────────────────────────────────── */ +.form-stack { + padding: 10px 26px 20px; + display: grid; + gap: 16px; +} + +.field { display: grid; gap: 6px; } + +.label { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); + display: block; +} .glass-input { border-radius: 14px; background: rgba(10, 8, 18, 0.42); - border: 1px solid rgba(192, 132, 252, 0.18); - overflow: visible; + border: 1px solid rgba(var(--accent-light-rgb), 0.18); + transition: border-color 0.2s; } -.custom-dropdown-wrap { - position: relative; - width: 140px; +.glass-input:focus-within { + border-color: rgba(var(--accent-rgb), 0.45); } -.dropdown-trigger { +.input { width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 14px; - cursor: pointer; - color: var(--text); - font-weight: 700; - font-size: 14px; - background: rgba(10, 8, 18, 0.42); - border: 1px solid rgba(192, 132, 252, 0.18); - border-radius: 14px; -} - -.dropdown-arrow { - font-size: 10px; - opacity: 0.6; -} - -.dropdown-menu { - position: absolute; - top: calc(100% + 8px); - left: 0; - right: 0; - z-index: 100; - display: flex; - flex-direction: column; - padding: 6px; - background: rgba(15, 12, 30, 0.95); - backdrop-filter: blur(20px); - border-radius: 16px; - border: 1px solid rgba(192, 132, 252, 0.3); - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); - animation: slideUp 0.2s ease-out forwards; -} - -.dropdown-item { - appearance: none; + padding: 12px 14px; + border: 0; + outline: none; background: transparent; - border: none; - padding: 10px 12px; - text-align: left; - color: rgba(245, 243, 255, 0.8); + color: var(--text); + font-size: 14px; + display: block; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Buttons +───────────────────────────────────────────────────────────────────────────── */ +.btn { + border-radius: 14px; + padding: 12px 14px; font-weight: 700; - font-size: 13px; - border-radius: 10px; cursor: pointer; - transition: all 0.2s ease; + border: 1px solid transparent; + font-family: inherit; + font-size: 14px; + transition: + transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), + filter 0.2s ease, + background 0.3s ease, + border-color 0.3s ease; } -.dropdown-item:hover { - background: rgba(168, 85, 247, 0.15); - color: #fff; -} - -.dropdown-item.active { - background: rgba(168, 85, 247, 0.25); - color: var(--purple2); -} - -.input { width: 100%; padding: 12px 14px; border: 0; outline: none; background: transparent; color: var(--text); font-size: 14px; } - -.btn { border-radius: 14px; padding: 12px 14px; font-weight: 700; cursor: pointer; border: 1px solid transparent; } .btn-primary { - background: linear-gradient(135deg, var(--purple0), var(--purple1), var(--purple2), var(--purple0)); + background: linear-gradient(135deg, var(--accent), var(--accent-mid), var(--accent-light), var(--accent)); background-size: 300% 300%; color: #000; width: 100%; - position: relative; - overflow: hidden; - box-shadow: 0 0 20px rgba(168, 85, 247, 0.4); + box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.4); animation: gradientFlow 4s ease infinite, pulseGlow 2s ease-in-out infinite; } -.btn-primary:hover { +.btn-primary:hover:not(:disabled) { transform: translateY(-2px) scale(1.02); - box-shadow: 0 0 35px rgba(168, 85, 247, 0.7); + box-shadow: 0 0 35px rgba(var(--accent-rgb), 0.7); filter: brightness(1.1); } +.btn-primary:active:not(:disabled) { + transform: translateY(0) scale(0.99); +} + .btn-primary:disabled { background: rgba(255, 255, 255, 0.05); color: rgba(255, 255, 255, 0.2); @@ -412,54 +542,352 @@ body.revoked-bg { } @keyframes gradientFlow { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } @keyframes pulseGlow { - 0%, 100% { box-shadow: 0 0 15px rgba(168, 85, 247, 0.4), inset 0 0 10px rgba(255, 255, 255, 0.1); } - 50% { box-shadow: 0 0 35px rgba(168, 85, 247, 0.8), inset 0 0 20px rgba(255, 255, 255, 0.2); } + 0%, 100% { box-shadow: 0 0 15px rgba(var(--accent-rgb), 0.4), inset 0 0 10px rgba(255,255,255,0.10); } + 50% { box-shadow: 0 0 35px rgba(var(--accent-rgb), 0.8), inset 0 0 20px rgba(255,255,255,0.20); } +} + +.btn-secondary, +.btn-tertiary { + background: rgba(10, 8, 18, 0.35); + border-color: rgba(var(--accent-light-rgb), 0.28); + color: #fff; +} + +.btn-secondary:hover, +.btn-tertiary:hover { + background: rgba(var(--accent-rgb), 0.10); + border-color: rgba(var(--accent-light-rgb), 0.45); +} + +/* Two-column grid row */ +.btn-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +/* Vertical stack (replaces the broken inline flexDirection hack) */ +.btn-stack { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 14px; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Status bar +───────────────────────────────────────────────────────────────────────────── */ +.status-bar { + padding: 14px; + display: flex; + justify-content: center; + gap: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.status-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; } -.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } -.btn-secondary, .btn-tertiary { background: rgba(10, 8, 18, 0.35); border-color: rgba(192, 132, 252, 0.28); color: #fff; } -.status-bar { padding: 14px; display: flex; justify-content: center; gap: 20px; border-top: 1px solid rgba(255, 255, 255, 0.05); } -.status-item { display: flex; align-items: center; gap: 6px; font-size: 12px; } .status-label { color: var(--muted); } -/* --- Modals --- */ -.modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); display: grid; place-items: center; z-index: 80; backdrop-filter: blur(4px); } -.modal { width: min(440px, 94vw); padding: 20px; } -.modal-title { margin: 0 0 16px; font-size: 20px; } - -/* --- Tutorials --- */ -.tutorial-overlay { position: fixed; inset: 0; z-index: 100; pointer-events: none; } -.tutorial-overlay * { pointer-events: auto; } -.tutorial-spotlight { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(2px); z-index: 101; transition: all 0.5s ease; } - -/* Spotlight Positions */ -.tutorial-spotlight.step-0 { clip-path: polygon(0% 0%, 0% 100%, 26px 100%, 26px 225px, calc(100% - 26px) 225px, calc(100% - 26px) 305px, 26px 305px, 26px 100%, 100% 100%, 100% 0%); } -.tutorial-spotlight.step-1 { clip-path: polygon(0% 0%, 0% 100%, 26px 100%, 26px 315px, calc(100% - 26px) 315px, calc(100% - 26px) 375px, 26px 375px, 26px 100%, 100% 100%, 100% 0%); } -.tutorial-spotlight.step-2 { clip-path: polygon(0% 0%, 0% 100%, calc(100% - 240px) 100%, calc(100% - 240px) 5px, calc(100% - 165px) 5px, calc(100% - 165px) 45px, calc(100% - 240px) 45px, calc(100% - 240px) 100%, 100% 100%, 100% 0%); } -.tutorial-spotlight.step-3 { clip-path: none; } - -.tutorial-card { position: absolute; width: 300px; padding: 20px; z-index: 105; transition: all 0.5s ease; box-shadow: 0 0 40px rgba(168, 85, 247, 0.3); } -.tutorial-card.step-0 { top: 315px; left: 50%; transform: translateX(-50%); } -.tutorial-card.step-1 { top: 385px; left: 50%; transform: translateX(-50%); } -.tutorial-card.step-2 { top: 55px; right: 10px; } -.tutorial-card.step-3 { top: 50%; left: 50%; transform: translate(-50%, -50%); } - -#step-username, #step-inject, #step-settings { position: relative; z-index: 102; } - -/* Global Smooth Transitions */ -button, input, select, .glass-card, .modal-overlay, .switch-ui { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +.status-active { + color: var(--accent-light); + text-shadow: 0 0 6px rgba(var(--accent-rgb), 0.5); } -.neon-text-soft { text-shadow: 0 0 8px rgba(168, 85, 247, 0.35); } +/* ───────────────────────────────────────────────────────────────────────────── + Modals +───────────────────────────────────────────────────────────────────────────── */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.70); + display: grid; + place-items: center; + z-index: 80; + backdrop-filter: blur(4px); + animation: fadeIn 0.15s ease; +} +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal { + width: min(440px, 94vw); + padding: 20px; + animation: slideUp 0.2s ease-out; +} + +.modal-title { + margin: 0 0 16px; + font-size: 20px; + font-weight: 800; +} + +.modal-p { + margin: 0 0 14px; + color: var(--muted); + font-size: 14px; + line-height: 1.6; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Settings rows +───────────────────────────────────────────────────────────────────────────── */ +.setting-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 10px 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.setting-row:last-of-type { border-bottom: none; } + +.setting-text { + display: grid; + gap: 3px; + min-width: 0; +} + +.setting-title { + font-weight: 800; + color: rgba(245, 243, 255, 0.88); + letter-spacing: 0.01em; + font-size: 14px; +} + +.setting-sub { + font-size: 12px; + color: var(--muted); + line-height: 1.4; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Toggle switch +───────────────────────────────────────────────────────────────────────────── */ +.switch { + position: relative; + width: 46px; + height: 28px; + flex: 0 0 auto; +} + +.switch input { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.switch-ui { + position: absolute; + inset: 0; + border-radius: 999px; + background: rgba(10, 8, 18, 0.35); + border: 1px solid rgba(var(--accent-light-rgb), 0.28); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); + transition: background 0.25s, border-color 0.25s, box-shadow 0.25s; +} + +.switch-ui::after { + content: ""; + position: absolute; + top: 50%; + left: 4px; + width: 20px; + height: 20px; + transform: translateY(-50%); + border-radius: 999px; + background: linear-gradient(135deg, rgba(245, 243, 255, 0.9), rgba(var(--accent-light-rgb), 0.55)); + box-shadow: 0 8px 18px rgba(0,0,0,0.35), 0 0 14px rgba(var(--accent-rgb), 0.18); + transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +.switch input:checked + .switch-ui { + background: rgba(var(--accent-rgb), 0.18); + border-color: rgba(var(--accent-light-rgb), 0.55); + box-shadow: + 0 0 0 4px rgba(var(--accent-rgb), 0.10), + 0 0 18px rgba(var(--accent-rgb), 0.14), + inset 0 1px 0 rgba(255,255,255,0.05); +} + +.switch input:checked + .switch-ui::after { + left: 22px; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Dropdown +───────────────────────────────────────────────────────────────────────────── */ +.custom-dropdown-wrap { + position: relative; + width: 140px; + /* ensure dropdown can overflow parent modal */ + z-index: 10; +} + +.dropdown-trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 13px; + cursor: pointer; + color: var(--text); + font-weight: 700; + font-size: 14px; + background: rgba(10, 8, 18, 0.42); + border: 1px solid rgba(var(--accent-light-rgb), 0.22); + border-radius: 12px; + font-family: inherit; + transition: border-color 0.2s; +} + +.dropdown-trigger:hover { + border-color: rgba(var(--accent-light-rgb), 0.42); +} + +.dropdown-arrow { + font-size: 10px; + opacity: 0.6; +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 200; + display: flex; + flex-direction: column; + padding: 6px; + background: rgba(15, 12, 30, 0.97); + backdrop-filter: blur(20px); + border-radius: 14px; + border: 1px solid rgba(var(--accent-light-rgb), 0.30); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); + animation: slideUp 0.15s ease-out forwards; +} + +.dropdown-item { + appearance: none; + background: transparent; + border: none; + padding: 9px 12px; + text-align: left; + color: rgba(245, 243, 255, 0.8); + font-weight: 700; + font-size: 13px; + border-radius: 10px; + cursor: pointer; + font-family: inherit; + transition: background 0.15s, color 0.15s; +} + +.dropdown-item:hover { + background: rgba(var(--accent-rgb), 0.15); + color: #fff; +} + +.dropdown-item.active { + background: rgba(var(--accent-rgb), 0.25); + color: var(--accent-light); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Theme swatch picker +───────────────────────────────────────────────────────────────────────────── */ +.theme-swatches { + display: flex; + gap: 8px; + align-items: center; +} + +.theme-swatch { + width: 24px; + height: 24px; + border-radius: 50%; + border: 2px solid transparent; + background: var(--swatch-color); + cursor: pointer; + padding: 0; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); + transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s; + flex: 0 0 auto; +} + +.theme-swatch:hover { + transform: scale(1.2); + box-shadow: 0 0 12px var(--swatch-color); +} + +.theme-swatch.active { + border-color: #fff; + transform: scale(1.2); + box-shadow: 0 0 14px var(--swatch-color); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Credits +───────────────────────────────────────────────────────────────────────────── */ +.credits-grid { + display: grid; + gap: 12px; + padding: 16px; + margin-bottom: 0; +} + +.credit-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; +} + +.credit-role { color: var(--muted); } +.credit-name { color: var(--text); font-weight: 600; } + +.credit-divider { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.08); + margin: 4px 0; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Revoked card variant +───────────────────────────────────────────────────────────────────────────── */ +.glass-card.revoked { + background: linear-gradient(180deg, rgba(255, 51, 51, 0.12), rgba(255, 51, 51, 0.06)); + border-color: rgba(255, 85, 85, 0.25); +} + +.glass-card.revoked .neon-ring::before { + background: radial-gradient(140px 90px at 20% 10%, rgba(255, 85, 85, 0.4), transparent 60%); +} + +.neon-text.red { + text-shadow: 0 0 12px rgba(255, 51, 51, 0.6); + color: var(--red0); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Animations +───────────────────────────────────────────────────────────────────────────── */ @keyframes slideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} + from { opacity: 0; transform: translateY(14px); } + to { opacity: 1; transform: translateY(0); } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 68f3ed4..e6c24e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,15 @@ import { useMemo, useState, useEffect } from "react"; type Status = - | "ready" - | "saved successfully" - | "injection started" - | "validating key..." - | "invalid key" - | "injecting..." - | "RL Running" - | "RL Closed" - | string; + | "ready" + | "saved successfully" + | "injection started" + | "validating key..." + | "invalid key" + | "injecting..." + | "RL Running" + | "RL Closed" + | string; interface UserInfo { userId: string | null; @@ -25,12 +25,24 @@ interface KeyValidationResponse { user: UserInfo | null; } +const THEMES = [ + { id: "phantom", label: "Phantom", color: "#a855f7" }, + { id: "glacier", label: "Glacier", color: "#38bdf8" }, + { id: "inferno", label: "Inferno", color: "#f97316" }, + { id: "matrix", label: "Matrix", color: "#00ff41" }, + { id: "synthwave", label: "Synthwave", color: "#f72585" }, + { id: "eclipse", label: "Eclipse", color: "#fbbf24" }, +] as const; + +type ThemeId = typeof THEMES[number]["id"]; + const LS_KEYS = { spoofed: "rlidentity.spoofedUsername", apiKey: "rlidentity.apiKey", minimizeToTray: "rlidentity.minimizeToTray", platform: "rlidentity.platform", autoInject: "rlidentity.autoInject", + theme: "rlidentity.theme", } as const; const GITHUB_URL = "https://git.rlidentity.me/bits/rlidentity"; @@ -56,7 +68,6 @@ async function tryInvoke(cmd: string, args?: Record) { async function openUrl(url: string) { const fallback = () => window.open(url, "_blank", "noopener,noreferrer"); if (!isTauriRuntime()) return fallback(); - try { const mod: any = await import("@tauri-apps/plugin-opener"); if (typeof mod.openUrl === "function") { @@ -70,30 +81,35 @@ async function openUrl(url: string) { } export default function App() { - const initialApiKey = useMemo(() => localStorage.getItem(LS_KEYS.apiKey) ?? "", []); - const initialSpoofed = useMemo(() => localStorage.getItem(LS_KEYS.spoofed) ?? "", []); - const initialMinToTray = useMemo(() => localStorage.getItem(LS_KEYS.minimizeToTray) === "true", []); - const initialPlatform = useMemo(() => localStorage.getItem(LS_KEYS.platform) ?? "Epic", []); + const initialApiKey = useMemo(() => localStorage.getItem(LS_KEYS.apiKey) ?? "", []); + const initialSpoofed = useMemo(() => localStorage.getItem(LS_KEYS.spoofed) ?? "", []); + const initialMinToTray = useMemo(() => localStorage.getItem(LS_KEYS.minimizeToTray) === "true", []); + const initialPlatform = useMemo(() => localStorage.getItem(LS_KEYS.platform) ?? "Epic", []); const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []); + const initialTheme = useMemo(() => (localStorage.getItem(LS_KEYS.theme) ?? "phantom") as ThemeId, []); - const [apiKey, setApiKey] = useState(initialApiKey); + const [apiKey, setApiKey] = useState(initialApiKey); const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed); - const [isAuthorized, setIsAuthorized] = useState(false); - const [isRevoked, setIsRevoked] = useState(false); - const [userData, setUserData] = useState(null); - - const [status, setStatus] = useState("ready"); - const [rlStatus, setRlStatus] = useState("Checking..."); - const [settingsOpen, setSettingsOpen] = useState(false); - const [logsOpen, setLogsOpen] = useState(false); - const [lastLog, setLastLog] = useState(""); + const [isAuthorized, setIsAuthorized] = useState(false); + const [isRevoked, setIsRevoked] = useState(false); + const [userData, setUserData] = useState(null); + + const [status, setStatus] = useState("ready"); + const [rlStatus, setRlStatus] = useState("Checking..."); + const [settingsOpen, setSettingsOpen] = useState(false); + const [logsOpen, setLogsOpen] = useState(false); + const [lastLog, setLastLog] = useState(""); const [minimizeToTray, setMinimizeToTray] = useState(initialMinToTray); - const [platform, setPlatform] = useState(initialPlatform); - const [autoInject, setAutoInject] = useState(initialAutoInject); + const [platform, setPlatform] = useState(initialPlatform); + const [autoInject, setAutoInject] = useState(initialAutoInject); const [platformPickerOpen, setPlatformPickerOpen] = useState(false); - - // Easter Egg State - const [debugOpen, setDebugOpen] = useState(false); + const [theme, setTheme] = useState(initialTheme); + + // Update modal + const [pendingUpdate, setPendingUpdate] = useState<{ version: string; install: () => Promise } | null>(null); + + // Easter egg + const [debugOpen, setDebugOpen] = useState(false); const [logoClicks, setLogoClicks] = useState(0); const handleLogoClick = (e: React.MouseEvent) => { @@ -112,29 +128,25 @@ export default function App() { return () => clearTimeout(timer); }, [logoClicks]); - // Tutorial State - const [tutorialStep, setTutorialStep] = useState(-1); - - // Startup Authorization & Update Check + // Apply theme useEffect(() => { - if (initialApiKey) { - authorize(initialApiKey); - } + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem(LS_KEYS.theme, theme); + }, [theme]); + + // Startup + useEffect(() => { + if (initialApiKey) authorize(initialApiKey); syncAssetsAndCheckUpdates(); }, []); async function syncAssetsAndCheckUpdates() { if (!isTauriRuntime()) return; - - // 1. Download DLL and Injector try { await tryInvoke("download_assets"); - console.log("Assets synced successfully"); } catch (e) { console.error("Failed to sync assets:", e); } - - // 2. Check for App updates checkForUpdates(); } @@ -144,89 +156,72 @@ export default function App() { const { check } = await import("@tauri-apps/plugin-updater"); const update = await check(); if (update) { - console.log(`Update available: ${update.version}`); - const confirmed = window.confirm(`A new version (${update.version}) is available. Would you like to update?`); - if (confirmed) { - setStatus("Updating..."); - await update.downloadAndInstall(); - // The app will restart automatically after install on some platforms, - // or we might need to relaunch. Tauri v2 updater usually handles this. - const { relaunch } = await import("@tauri-apps/plugin-process"); - await relaunch(); - } + setPendingUpdate({ + version: update.version, + install: async () => { + setStatus("Updating..."); + await update.downloadAndInstall(); + const { relaunch } = await import("@tauri-apps/plugin-process"); + await relaunch(); + }, + }); } } catch (e) { console.error("Failed to check for updates:", e); } } - // Sync revoked background to body + // Revoked bg useEffect(() => { - if (isRevoked) { - document.body.classList.add('revoked-bg'); - } else { - document.body.classList.remove('revoked-bg'); - } + document.body.classList.toggle("revoked-bg", isRevoked); }, [isRevoked]); - // Poll for Rocket League status & Auto Inject + // Poll RL status + auto-inject useEffect(() => { if (!isAuthorized || isRevoked) return; const interval = setInterval(async () => { - try { - const res = await tryInvoke<{is_running: boolean}>("check_status"); - if (res) { - const wasRunning = rlStatus === "RL Running"; - const isRunning = res.is_running; - setRlStatus(isRunning ? "RL Running" : "RL Closed"); - - // Auto Inject Logic: if it just started running and autoInject is on - if (!wasRunning && isRunning && autoInject) { - inject(); - } - } - } catch (e) { - console.error(e); + try { + const res = await tryInvoke<{ is_running: boolean }>("check_status"); + if (res) { + const wasRunning = rlStatus === "RL Running"; + const isRunning = res.is_running; + setRlStatus(isRunning ? "RL Running" : "RL Closed"); + if (!wasRunning && isRunning && autoInject) inject(); } + } catch (e) { + console.error(e); + } }, 2000); return () => clearInterval(interval); }, [isAuthorized, isRevoked, rlStatus, autoInject]); async function authorize(keyToTry: string) { - if (!keyToTry.trim()) { - setStatus("Please enter a key"); - return; - } + if (!keyToTry.trim()) { setStatus("Please enter a key"); return; } setStatus("validating key..."); setIsRevoked(false); try { - const hwid = await tryInvoke("get_hwid") || "UNKNOWN-HWID"; - const res = await tryInvoke("validate_key", { key: keyToTry.trim(), hwid }); - - if (res && res.status === "valid") { - localStorage.setItem(LS_KEYS.apiKey, keyToTry.trim()); - setUserData(res.user); - setIsAuthorized(true); - setIsRevoked(false); - setStatus("ready"); - - // Check for tutorial - if (res.user?.logins === 0) { - setTutorialStep(0); - } - } else if (res && res.status === "revoked") { - setIsRevoked(true); - setIsAuthorized(false); - setStatus("Error: Key Revoked"); - } else if (res && res.status === "invalid_hwid") { - setStatus("Error: Key locked to another PC"); - setIsAuthorized(false); - } else { - setStatus("Error: Invalid key"); - setIsAuthorized(false); - } - } catch (e) { - setStatus("Network Error: Check connection"); + const hwid = await tryInvoke("get_hwid") || "UNKNOWN-HWID"; + const res = await tryInvoke("validate_key", { key: keyToTry.trim(), hwid }); + + if (res?.status === "valid") { + localStorage.setItem(LS_KEYS.apiKey, keyToTry.trim()); + setUserData(res.user); + setIsAuthorized(true); + setIsRevoked(false); + setStatus("ready"); + } else if (res?.status === "revoked") { + setIsRevoked(true); + setIsAuthorized(false); + setStatus("Error: Key Revoked"); + } else if (res?.status === "invalid_hwid") { + setStatus("Error: Key locked to another PC"); + setIsAuthorized(false); + } else { + setStatus("Error: Invalid key"); + setIsAuthorized(false); + } + } catch { + setStatus("Network Error: Check connection"); } } @@ -234,25 +229,25 @@ export default function App() { localStorage.setItem(LS_KEYS.spoofed, spoofedUsername.trim()); localStorage.setItem(LS_KEYS.platform, platform); try { - await tryInvoke("save_config", { name: spoofedUsername.trim(), platform }); - setStatus("saved successfully"); - window.setTimeout(() => setStatus("ready"), 1400); + await tryInvoke("save_config", { name: spoofedUsername.trim(), platform }); + setStatus("saved successfully"); + window.setTimeout(() => setStatus("ready"), 1400); } catch (e) { - setStatus("Save error: " + String(e)); + setStatus("Save error: " + String(e)); } } async function inject() { setStatus("injecting..."); try { - const res = await tryInvoke("inject_dll", { discordId: userData?.discordId }); - setLastLog(res || "Successfully Injected!"); - setStatus("Successfully Injected!"); - window.setTimeout(() => setStatus("ready"), 1400); + const res = await tryInvoke("inject_dll", { discordId: userData?.discordId }); + setLastLog(res || "Successfully Injected!"); + setStatus("Successfully Injected!"); + window.setTimeout(() => setStatus("ready"), 1400); } catch (e) { - setStatus("Injection Failed!"); - setLastLog(String(e)); - setLogsOpen(true); + setStatus("Injection Failed!"); + setLastLog(String(e)); + setLogsOpen(true); } } @@ -260,7 +255,7 @@ export default function App() { setMinimizeToTray(next); localStorage.setItem(LS_KEYS.minimizeToTray, String(next)); } - + function toggleAutoInject(next: boolean) { setAutoInject(next); localStorage.setItem(LS_KEYS.autoInject, String(next)); @@ -271,7 +266,7 @@ export default function App() { if (minimizeToTray) { await tryInvoke("minimize_to_tray"); } else { - await tryWindowApi((w) => w.minimize()); + await tryWindowApi(w => w.minimize()); } } @@ -280,41 +275,51 @@ export default function App() { window.location.reload(); }; - const tutorialSteps = [ - { title: "Welcome to RLidentity", text: "Let's show you around. First, enter your spoofed username here.", target: "input" }, - { title: "Injection", text: "Once Rocket League is running, click Inject to start. Or enable Auto-Inject in settings!", target: "btn-primary" }, - { title: "Settings", text: "Customize your experience here. Change your platform or toggle Auto-Injection.", target: "tb-action" }, - { title: "All Set!", text: "You're ready to win. Happy gaming!", target: "none" } - ]; + const closeAllModals = () => { + setSettingsOpen(false); + setLogsOpen(false); + setDebugOpen(false); + }; + const isModalOpen = settingsOpen || logsOpen || debugOpen; + + // ── Auth screen ─────────────────────────────────────────────────────────── if (!isAuthorized) { return (
- @@ -349,234 +357,235 @@ export default function App() { ); } + // ── Main app ────────────────────────────────────────────────────────────── return ( -
-