Initial Tauri GUI (Source)

This commit is contained in:
bits 2026-03-18 02:58:27 +03:00
commit 33bf07ec46
24 changed files with 9990 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

7
README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

7
src-tauri/.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

27
src-tauri/Cargo.toml Normal file
View 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
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View 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"
]
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
tsconfig.json Normal file
View 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
View 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
View 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/**"],
},
},
}));