Initial Tauri GUI (Source)
This commit is contained in:
commit
33bf07ec46
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||||
|
}
|
||||||
7
README.md
Normal file
7
README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
14
index.html
Normal file
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>RLidentity</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2157
package-lock.json
generated
Normal file
2157
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "rlidentitygui",
|
||||||
|
"private": true,
|
||||||
|
"version": "2.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.10.1",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"autoprefixer": "^10.4.27",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/rlidentity.webp
Normal file
BIN
public/rlidentity.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
7
src-tauri/.gitignore
vendored
Normal file
7
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
6259
src-tauri/Cargo.lock
generated
Normal file
6259
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
src-tauri/Cargo.toml
Normal file
27
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "rlidentitygui"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rlidentitygui_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { version = '0.12', features = ['json'] }
|
||||||
|
tokio = { version = '1', features = ['full'] }
|
||||||
|
window-vibrancy = "0.5.2"
|
||||||
|
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sysinfo = "0.30"
|
||||||
|
dirs = "5"
|
||||||
|
tauri-plugin-updater = "2.10.0"
|
||||||
|
tauri-plugin-process = "2.3.1"
|
||||||
|
|
||||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
19
src-tauri/capabilities/default.json
Normal file
19
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-hide",
|
||||||
|
"core:window:allow-show",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"opener:default",
|
||||||
|
"updater:default",
|
||||||
|
"process:allow-restart",
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
src-tauri/permissions/main.toml
Normal file
17
src-tauri/permissions/main.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[default]
|
||||||
|
description = "Default permissions for my application"
|
||||||
|
permissions = [
|
||||||
|
"allow-main-commands"
|
||||||
|
]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-main-commands"
|
||||||
|
description = "Allows all main commands"
|
||||||
|
commands.allow = [
|
||||||
|
"minimize_to_tray",
|
||||||
|
"save_config",
|
||||||
|
"inject_dll",
|
||||||
|
"validate_key",
|
||||||
|
"check_status",
|
||||||
|
"get_hwid"
|
||||||
|
]
|
||||||
235
src-tauri/src/lib.rs
Normal file
235
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
use reqwest;
|
||||||
|
use tauri::{Manager, WebviewWindow};
|
||||||
|
use window_vibrancy::apply_acrylic;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fs;
|
||||||
|
use sysinfo::System;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub is_running: bool,
|
||||||
|
pub is_injected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct UserInfo {
|
||||||
|
pub userId: Option<String>,
|
||||||
|
pub discordId: Option<String>,
|
||||||
|
pub epicId: Option<String>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub globalName: Option<String>,
|
||||||
|
pub logins: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct KeyValidationResponse {
|
||||||
|
pub status: String,
|
||||||
|
pub user: Option<UserInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn minimize_to_tray(window: WebviewWindow) {
|
||||||
|
let _ = window.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn validate_key(key: String, hwid: String) -> Result<KeyValidationResponse, String> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let res = 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
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}),
|
||||||
|
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()),
|
||||||
|
globalName: u.get("globalName").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||||
|
logins: u.get("logins").and_then(|v| v.as_i64()).map(|n| n as i32),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(KeyValidationResponse { status, user })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn save_config(name: String, platform: String) -> 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");
|
||||||
|
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"spoofedName": name,
|
||||||
|
"platform": platform
|
||||||
|
});
|
||||||
|
|
||||||
|
fs::write(path, serde_json::to_string_pretty(&json).unwrap()).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn check_status() -> Status {
|
||||||
|
let mut s = System::new_all();
|
||||||
|
s.refresh_processes();
|
||||||
|
let is_running = s.processes_by_exact_name("RocketLeague.exe").next().is_some();
|
||||||
|
Status { is_running, is_injected: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn inject_dll(_discordId: Option<String>) -> Result<String, String> {
|
||||||
|
let injector_path = "E:\\projects\\Rocket League\\RLIdentityDLL\\x64\\Release\\injector.exe";
|
||||||
|
let dll_path = "E:\\projects\\Rocket League\\RLIdentityDLL\\x64\\Release\\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 !Path::new(injector_path).exists() {
|
||||||
|
return Err(format!("Injector missing: {}", injector_path));
|
||||||
|
}
|
||||||
|
if !Path::new(dll_path).exists() {
|
||||||
|
return Err(format!("DLL missing: {}", dll_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.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,
|
||||||
|
get_hwid
|
||||||
|
])
|
||||||
|
.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::tray::TrayIconBuilder::new()
|
||||||
|
.icon(icon)
|
||||||
|
.menu(&tray_menu)
|
||||||
|
.on_menu_event(move |_app, event| {
|
||||||
|
if event.id().as_ref() == "tray_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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(app)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
6
src-tauri/src/main.rs
Normal file
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
rlidentitygui_lib::run()
|
||||||
|
}
|
||||||
48
src-tauri/tauri.conf.json
Normal file
48
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "rlidentitygui",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"identifier": "me.rlidentity.gui",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "RLidentity",
|
||||||
|
"width": 600,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": false,
|
||||||
|
"fullscreen": false,
|
||||||
|
"transparent": false,
|
||||||
|
"decorations": false,
|
||||||
|
"devtools": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"endpoints": [
|
||||||
|
"https://api.rlidentity.me/version"
|
||||||
|
],
|
||||||
|
"pubkey": "DWY+YmX5uY2E3N0N3Q0N3Q0N3Q0N3Q0N3Q0N3Q0N3Q0N3Q=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
465
src/App.css
Normal file
465
src/App.css
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
/* --- Premium dark + neon purple glassmorphism theme (RLidentity) --- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family:
|
||||||
|
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));
|
||||||
|
color: var(--text);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.revoked-bg {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%);
|
||||||
|
filter: blur(12px);
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: float 10s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
||||||
|
50% { transform: translate3d(0, -10px, 0) scale(1.02); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- App Shell --- */
|
||||||
|
.app-shell {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Titlebar --- */
|
||||||
|
.window-titlebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 12px;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(10, 8, 18, 0.4);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-titlebar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-logo {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(168, 85, 247, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar-app-name {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: rgba(245, 243, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar-controls-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-action {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid rgba(192, 132, 252, 0.22);
|
||||||
|
background: rgba(10, 8, 18, 0.22);
|
||||||
|
color: rgba(245, 243, 255, 0.82);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.10);
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win-btn {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid rgba(192, 132, 252, 0.20);
|
||||||
|
background: rgba(10, 8, 18, 0.28);
|
||||||
|
color: rgba(245, 243, 255, 0.82);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Welcome Section --- */
|
||||||
|
.welcome-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
animation: slideUp 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(245, 243, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id-badge {
|
||||||
|
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);
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--purple2);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Main Card --- */
|
||||||
|
.panel-wrap {
|
||||||
|
width: min(520px, 94vw);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
border-radius: 22px;
|
||||||
|
background: linear-gradient(180deg, var(--glass-2), var(--glass));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-ring { position: relative; }
|
||||||
|
.neon-ring::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: radial-gradient(140px 90px at 20% 10%, rgba(192, 132, 252, 0.35), transparent 60%);
|
||||||
|
filter: blur(10px);
|
||||||
|
opacity: 0.85;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.glass-input {
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(10, 8, 18, 0.42);
|
||||||
|
border: 1px solid rgba(192, 132, 252, 0.18);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dropdown-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
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;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
color: rgba(245, 243, 255, 0.8);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s 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-size: 300% 300%;
|
||||||
|
color: #000;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 20px rgba(168, 85, 247, 0.4);
|
||||||
|
animation: gradientFlow 4s ease infinite, pulseGlow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px) scale(1.02);
|
||||||
|
box-shadow: 0 0 35px rgba(168, 85, 247, 0.7);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: none;
|
||||||
|
animation: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientFlow {
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-text-soft { text-shadow: 0 0 8px rgba(168, 85, 247, 0.35); }
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
567
src/App.tsx
Normal file
567
src/App.tsx
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
type Status =
|
||||||
|
| "ready"
|
||||||
|
| "saved successfully"
|
||||||
|
| "injection started"
|
||||||
|
| "validating key..."
|
||||||
|
| "invalid key"
|
||||||
|
| "injecting..."
|
||||||
|
| "RL Running"
|
||||||
|
| "RL Closed"
|
||||||
|
| string;
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
userId: string | null;
|
||||||
|
discordId: string | null;
|
||||||
|
epicId: string | null;
|
||||||
|
username: string | null;
|
||||||
|
globalName: string | null;
|
||||||
|
logins?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyValidationResponse {
|
||||||
|
status: string;
|
||||||
|
user: UserInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LS_KEYS = {
|
||||||
|
spoofed: "neonGlass.spoofedUsername",
|
||||||
|
apiKey: "neonGlass.apiKey",
|
||||||
|
minimizeToTray: "neonGlass.minimizeToTray",
|
||||||
|
platform: "neonGlass.platform",
|
||||||
|
autoInject: "neonGlass.autoInject",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const GITHUB_URL = "https://github.com/RLidentity";
|
||||||
|
const FAQ_URL = "https://rlidentity.me/faq";
|
||||||
|
|
||||||
|
function isTauriRuntime() {
|
||||||
|
return typeof window !== "undefined" && typeof (window as any).__TAURI_INTERNALS__ !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryWindowApi(action: (win: any) => Promise<void>) {
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
|
const mod: any = await import("@tauri-apps/api/window");
|
||||||
|
const win = mod.getCurrentWindow();
|
||||||
|
await action(win);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryInvoke<T>(cmd: string, args?: Record<string, unknown>) {
|
||||||
|
if (!isTauriRuntime()) return null;
|
||||||
|
const mod: any = await import("@tauri-apps/api/core");
|
||||||
|
return (await mod.invoke(cmd, args)) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
await mod.openUrl(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fallback();
|
||||||
|
} catch {
|
||||||
|
fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []);
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState(initialApiKey);
|
||||||
|
const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed);
|
||||||
|
const [isAuthorized, setIsAuthorized] = useState(false);
|
||||||
|
const [isRevoked, setIsRevoked] = useState(false);
|
||||||
|
const [userData, setUserData] = useState<UserInfo | null>(null);
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<Status>("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 [platformPickerOpen, setPlatformPickerOpen] = useState(false);
|
||||||
|
|
||||||
|
// Easter Egg State
|
||||||
|
const [debugOpen, setDebugOpen] = useState(false);
|
||||||
|
const [logoClicks, setLogoClicks] = useState(0);
|
||||||
|
|
||||||
|
const handleLogoClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setLogoClicks(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logoClicks >= 5) {
|
||||||
|
setDebugOpen(true);
|
||||||
|
setLogoClicks(0);
|
||||||
|
}
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (logoClicks > 0) setLogoClicks(0);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [logoClicks]);
|
||||||
|
|
||||||
|
// Tutorial State
|
||||||
|
const [tutorialStep, setTutorialStep] = useState(-1);
|
||||||
|
|
||||||
|
// Startup Authorization & Update Check
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialApiKey) {
|
||||||
|
authorize(initialApiKey);
|
||||||
|
}
|
||||||
|
checkForUpdates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
|
try {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to check for updates:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync revoked background to body
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRevoked) {
|
||||||
|
document.body.classList.add('revoked-bg');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('revoked-bg');
|
||||||
|
}
|
||||||
|
}, [isRevoked]);
|
||||||
|
|
||||||
|
// Poll for Rocket League 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);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isAuthorized, isRevoked, rlStatus, autoInject]);
|
||||||
|
|
||||||
|
async function authorize(keyToTry: string) {
|
||||||
|
if (!keyToTry.trim()) {
|
||||||
|
setStatus("Please enter a key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("validating key...");
|
||||||
|
setIsRevoked(false);
|
||||||
|
try {
|
||||||
|
const hwid = await tryInvoke<string>("get_hwid") || "UNKNOWN";
|
||||||
|
const res = await tryInvoke<KeyValidationResponse>("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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
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);
|
||||||
|
} catch (e) {
|
||||||
|
setStatus("Save error: " + String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inject() {
|
||||||
|
setStatus("injecting...");
|
||||||
|
try {
|
||||||
|
const res = await tryInvoke<string>("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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMinimizeToTray(next: boolean) {
|
||||||
|
setMinimizeToTray(next);
|
||||||
|
localStorage.setItem(LS_KEYS.minimizeToTray, String(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAutoInject(next: boolean) {
|
||||||
|
setAutoInject(next);
|
||||||
|
localStorage.setItem(LS_KEYS.autoInject, String(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMinimizeClick() {
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
|
if (minimizeToTray) {
|
||||||
|
await tryInvoke("minimize_to_tray");
|
||||||
|
} else {
|
||||||
|
await tryWindowApi((w) => w.minimize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem(LS_KEYS.apiKey);
|
||||||
|
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" }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isAuthorized) {
|
||||||
|
return (
|
||||||
|
<div className="app-shell">
|
||||||
|
<div className={`bg-aurora ${isRevoked ? 'revoked-aurora' : ''}`} aria-hidden="true" />
|
||||||
|
|
||||||
|
<div className="window-titlebar" data-tauri-drag-region>
|
||||||
|
<div className="window-titlebar-left">
|
||||||
|
<img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
|
||||||
|
<div className="titlebar-text" data-tauri-drag-region>
|
||||||
|
<div className="app-name neon-text-soft">RLidentity <span style={{ fontSize: '10px', opacity: 0.6, marginLeft: '4px' }}>v2.0.0</span></div>
|
||||||
|
<div className="app-slogan">{isRevoked ? 'License Revoked' : 'Authorize to continue'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="titlebar-controls">
|
||||||
|
<button className="win-btn" onClick={handleMinimizeClick}>—</button>
|
||||||
|
<button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="panel-wrap">
|
||||||
|
<section className={`glass-card neon-ring ${isRevoked ? 'revoked' : ''}`} style={{ maxWidth: '400px', margin: 'auto' }}>
|
||||||
|
<header className="card-header">
|
||||||
|
<h1 className={`headline neon-text ${isRevoked ? 'red' : ''}`}>
|
||||||
|
{isRevoked ? 'ACCESS REVOKED' : 'RLidentity'}
|
||||||
|
</h1>
|
||||||
|
<p className="app-slogan">
|
||||||
|
{isRevoked ? 'This license is no longer active' : 'Enter your API key to continue'}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div className="form-stack">
|
||||||
|
{!isRevoked && (
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">License Key</label>
|
||||||
|
<div className="glass-input">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
placeholder="Enter your license key..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="btn btn-primary" onClick={() => isRevoked ? logout() : authorize(apiKey)}>
|
||||||
|
{isRevoked ? 'Change Key' : 'Login'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{status !== "ready" && (
|
||||||
|
<p className="status-value" style={{textAlign:'center', marginTop:'10px', color: (isRevoked || status.startsWith('Error')) ? '#ff5555' : 'inherit'}}>
|
||||||
|
{status}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-shell">
|
||||||
|
<div className="bg-aurora" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div className="window-titlebar" data-tauri-drag-region>
|
||||||
|
<div className="window-titlebar-left">
|
||||||
|
<img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
|
||||||
|
<div className="titlebar-app-name neon-text-soft" data-tauri-drag-region>RLidentity</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="titlebar-controls-wrap" data-tauri-drag-region>
|
||||||
|
<div className="titlebar-controls">
|
||||||
|
<button id="step-settings" className="tb-action" onClick={() => setSettingsOpen(true)}>Settings</button>
|
||||||
|
<button className="tb-action" onClick={() => openUrl(GITHUB_URL)}>GitHub</button>
|
||||||
|
<button className="win-btn" onClick={handleMinimizeClick}>—</button>
|
||||||
|
<button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="panel-wrap">
|
||||||
|
<header className="welcome-section">
|
||||||
|
<h2 className="welcome-text">Welcome, <span className="neon-text-soft">{userData?.globalName || userData?.username || "User"}</span></h2>
|
||||||
|
<div className="user-id-badge">User #{userData?.userId || "0"}</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="glass-card neon-ring">
|
||||||
|
<header className="card-header">
|
||||||
|
<h1 className="headline neon-text">Be anyone, Win everything</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="form-stack">
|
||||||
|
<div className="field" id="step-username">
|
||||||
|
<label className="label">spoofed username</label>
|
||||||
|
<div className="glass-input">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={spoofedUsername}
|
||||||
|
onChange={(e) => setSpoofedUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="step-inject"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={inject}
|
||||||
|
disabled={rlStatus !== "RL Running" && tutorialStep !== 1}
|
||||||
|
>
|
||||||
|
{rlStatus === "RL Running" ? "Inject" : "Start Rocket League"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="btn-row">
|
||||||
|
<button className="btn btn-secondary" onClick={() => openUrl(FAQ_URL)}>FAQ</button>
|
||||||
|
<button className="btn btn-tertiary" onClick={saveConfig}>Save Name</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="status-bar">
|
||||||
|
<div className="status-item">
|
||||||
|
<span className="status-label">status</span>
|
||||||
|
<span className="status-value">{status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-item">
|
||||||
|
<span className="status-label">game</span>
|
||||||
|
<span className="status-value">{rlStatus}</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{(settingsOpen || logsOpen || debugOpen) && (
|
||||||
|
<div className="modal-overlay" onClick={() => { setSettingsOpen(false); setLogsOpen(false); setDebugOpen(false); }}>
|
||||||
|
<div className="modal glass-card neon-ring" onClick={e => e.stopPropagation()} style={{ maxWidth: (logsOpen || debugOpen) ? '600px' : '400px' }}>
|
||||||
|
{debugOpen ? (
|
||||||
|
<>
|
||||||
|
<h2 className="modal-title neon-text-soft">System Credits & Debug</h2>
|
||||||
|
<div className="glass-input" style={{ padding: '15px', marginBottom: '15px' }}>
|
||||||
|
<div style={{ display: 'grid', gap: '10px', fontSize: '13px' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Lead Dev & Owner:</span>
|
||||||
|
<span className="neon-text-soft">Bits</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Dev & Admin:</span>
|
||||||
|
<span style={{ color: '#fff' }}>Danni</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Co-Owner:</span>
|
||||||
|
<span style={{ color: '#fff' }}>Deniz</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Administrator:</span>
|
||||||
|
<span style={{ color: '#fff' }}>Kairo</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Helpers:</span>
|
||||||
|
<span style={{ color: '#fff' }}>Quinn, SNDR</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Tester:</span>
|
||||||
|
<span style={{ color: '#fff' }}>Emir</span>
|
||||||
|
</div>
|
||||||
|
<hr style={{ border: 'none', borderTop: '1px solid rgba(255,255,255,0.1)', margin: '5px 0' }} />
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ width: '100%', marginTop: '5px' }}
|
||||||
|
onClick={() => openUrl('https://rlidentity.me/discord')}
|
||||||
|
>
|
||||||
|
Join Discord (rlidentity.me/discord)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={() => setDebugOpen(false)}>Close Debug</button>
|
||||||
|
</>
|
||||||
|
) : logsOpen ? (
|
||||||
|
<>
|
||||||
|
<h2 className="modal-title neon-text-soft">Injection Logs</h2>
|
||||||
|
<div className="glass-input" style={{ height: '300px', padding: '10px', overflowY: 'auto' }}>
|
||||||
|
<pre style={{ fontSize: '12px', color: '#fff', whiteSpace: 'pre-wrap' }}>
|
||||||
|
{lastLog || "No logs yet..."}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={() => setLogsOpen(false)}>Close Logs</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="modal-title neon-text-soft">Settings</h2>
|
||||||
|
|
||||||
|
<div className="setting-row">
|
||||||
|
<div className="setting-text">
|
||||||
|
<div className="setting-title">Platform</div>
|
||||||
|
</div>
|
||||||
|
<div className="custom-dropdown-wrap">
|
||||||
|
<button
|
||||||
|
className="glass-input dropdown-trigger"
|
||||||
|
onClick={() => setPlatformPickerOpen(!platformPickerOpen)}
|
||||||
|
>
|
||||||
|
<span className="dropdown-value">{platform === "Epic" ? "Epic Games" : "Steam"}</span>
|
||||||
|
<span className="dropdown-arrow">▾</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{platformPickerOpen && (
|
||||||
|
<div className="dropdown-menu glass-card neon-ring">
|
||||||
|
<button
|
||||||
|
className={`dropdown-item ${platform === "Epic" ? "active" : ""}`}
|
||||||
|
onClick={() => { setPlatform("Epic"); setPlatformPickerOpen(false); }}
|
||||||
|
>
|
||||||
|
Epic Games
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`dropdown-item ${platform === "Steam" ? "active" : ""}`}
|
||||||
|
onClick={() => { setPlatform("Steam"); setPlatformPickerOpen(false); }}
|
||||||
|
>
|
||||||
|
Steam
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-row" id="step-autoinject">
|
||||||
|
<div className="setting-text">
|
||||||
|
<div className="setting-title">Auto Injection</div>
|
||||||
|
<div className="setting-sub">Injects automatically when RL starts</div>
|
||||||
|
</div>
|
||||||
|
<label className="switch">
|
||||||
|
<input type="checkbox" checked={autoInject} onChange={e => toggleAutoInject(e.target.checked)} />
|
||||||
|
<span className="switch-ui" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-row">
|
||||||
|
<div className="setting-text">
|
||||||
|
<div className="setting-title">Minimize to tray</div>
|
||||||
|
</div>
|
||||||
|
<label className="switch">
|
||||||
|
<input type="checkbox" checked={minimizeToTray} onChange={e => toggleMinimizeToTray(e.target.checked)} />
|
||||||
|
<span className="switch-ui" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="btn-row" style={{ flexDirection: 'column', gap: '10px', marginTop: '10px' }}>
|
||||||
|
<button className="btn btn-tertiary" onClick={() => { setSettingsOpen(false); setLogsOpen(true); }}>View Last Injection Log</button>
|
||||||
|
<button className="btn btn-secondary" onClick={logout}>Logout</button>
|
||||||
|
<button className="btn btn-primary" onClick={() => setSettingsOpen(false)}>Close</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tutorialStep >= 0 && (
|
||||||
|
<div className="tutorial-overlay">
|
||||||
|
<div className={`tutorial-spotlight step-${tutorialStep}`} />
|
||||||
|
<div className={`tutorial-card glass-card neon-ring step-${tutorialStep}`}>
|
||||||
|
<h2 className="modal-title neon-text">{tutorialSteps[tutorialStep].title}</h2>
|
||||||
|
<p className="modal-p">{tutorialSteps[tutorialStep].text}</p>
|
||||||
|
<div className="btn-row">
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setTutorialStep(-1)}
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (tutorialStep === 2) {
|
||||||
|
setSettingsOpen(true);
|
||||||
|
}
|
||||||
|
if (tutorialStep < tutorialSteps.length - 1) {
|
||||||
|
setTutorialStep(tutorialStep + 1);
|
||||||
|
} else {
|
||||||
|
setSettingsOpen(false);
|
||||||
|
setTutorialStep(-1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tutorialStep < tutorialSteps.length - 1 ? "Next" : "Finish"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
32
src/main.tsx
Normal file
32
src/main.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
function showFatal(message: string) {
|
||||||
|
const el = document.createElement("pre");
|
||||||
|
el.style.whiteSpace = "pre-wrap";
|
||||||
|
el.style.padding = "16px";
|
||||||
|
el.style.margin = "0";
|
||||||
|
el.style.height = "100vh";
|
||||||
|
el.style.boxSizing = "border-box";
|
||||||
|
el.style.background = "#07060b";
|
||||||
|
el.style.color = "white";
|
||||||
|
el.textContent = message;
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("error", (e) => {
|
||||||
|
showFatal(`window.error:\n${e.message}\n\n${String((e as ErrorEvent).error?.stack ?? "")}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unhandledrejection", (e) => {
|
||||||
|
showFatal(`unhandledrejection:\n${String((e as PromiseRejectionEvent).reason)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
31
vite.config.ts
Normal file
31
vite.config.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [react()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent Vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
host: host || false,
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: "ws",
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
Loading…
x
Reference in New Issue
Block a user