Making the source code public
Finally ! here's the source code
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Rust
|
||||
/src-tauri/target/
|
||||
/src-tauri/Cargo.lock
|
||||
|
||||
# Node
|
||||
/node_modules/
|
||||
/dist/
|
||||
/npm-debug.log*
|
||||
/yarn-debug.log*
|
||||
/yarn-error.log*
|
||||
/lerna-debug.log*
|
||||
/.pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode*
|
||||
.idea*
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build output
|
||||
/app-icon.png
|
||||
/src-tauri/icons/*.png
|
||||
/src-tauri/icons/*.ico
|
||||
/src-tauri/icons/*.icns
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
|
@ -22,7 +22,9 @@
|
|||
---
|
||||
|
||||
### PROJECT OVERVIEW
|
||||
Emerald Legacy Launcher started as a solo project to create a better way to play Legacy Console Edition. Over time, other developers joined the effort, bringing expertise from various preservation projects into one place. By combining these efforts into a single unified launcher, we aim to build the best possible experience for Legacy Console Edition on PC.
|
||||
Emerald Legacy Launcher started as a solo project to create a better way to play Legacy Console Edition. Over time, other developers joined the effort, bringing expertise from various preservation projects into one place.
|
||||
|
||||
**Our Philosophy:** For too long, the LCE scene has been fragmented across different, often resource-heavy launchers. Emerald was built to stop this fragmentation and centralize everything into a single, definitive hub. By avoiding bloated frameworks and utilizing a modern Rust/Tauri stack, we deliver a high-performance, cross-platform experience that uses only ~15MB of RAM—leaving all your PC's resources dedicated to the game itself.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -67,3 +69,4 @@ Click the heading above to track progress, view active tasks, and see upcoming l
|
|||
---
|
||||
|
||||
*Developed by The Emerald Team and the Legacy Console Edition Community.*
|
||||
***This project is licensed under the GNU GPL v3 License - see the LICENSE file for details.***
|
||||
12
index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Emerald Legacy</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3565
package-lock.json
generated
Normal file
30
package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "emerald-legacy-launcher",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"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.8",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/fonts/CHS/MSYH.ttf
Normal file
BIN
public/fonts/CHT/DFHeiMedium-B5.ttf
Normal file
BIN
public/fonts/CHT/DFTT_R5.TTC
Normal file
BIN
public/fonts/JPN/DF-DotDotGothic16.ttf
Normal file
BIN
public/fonts/JPN/DFGMaruGothic-Md.ttf
Normal file
BIN
public/fonts/KOR/BOKMSD.ttf
Normal file
BIN
public/fonts/KOR/candadite2.ttf
Normal file
BIN
public/fonts/Mojang Font_11.ttf
Normal file
BIN
public/fonts/Mojang Font_7.ttf
Normal file
BIN
public/fonts/Mojangles.ttf
Normal file
BIN
public/fonts/Mojangles_11.abc
Normal file
BIN
public/fonts/Mojangles_7.abc
Normal file
BIN
public/fonts/RU/SpaceMace.ttf
Normal file
BIN
public/fonts/chars.txt
Normal file
BIN
public/images/MenuTitle.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
public/images/dirt-background.png
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
public/music/music1.ogg
Normal file
BIN
public/music/music2.ogg
Normal file
BIN
public/music/music3.ogg
Normal file
BIN
public/music/music4.ogg
Normal file
BIN
public/music/music5.ogg
Normal file
BIN
public/sounds/back.ogg
Normal file
BIN
public/sounds/click.wav
Normal file
BIN
public/sounds/levelup.ogg
Normal file
BIN
public/sounds/orb.ogg
Normal file
BIN
public/sounds/pop.wav
Normal file
BIN
public/sounds/wood click.wav
Normal file
23
src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "emerald-legacy-launcher"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "emerald_legacy_launcher_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.11", features = ["stream"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures-util = "0.3" #
|
||||
tokio-util = { version = "0.7.18", features = ["rt"] }
|
||||
3
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
18
src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"allow": [
|
||||
{ "url": "https://github.com/KayJannOnGit/*" },
|
||||
{ "url": "https://discord.gg/*" },
|
||||
{ "url": "https://reddit.com/*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","opener:default",{"identifier":"opener:allow-open-url","allow":[{"url":"https://github.com/KayJannOnGit/*"},{"url":"https://discord.gg/*"},{"url":"https://reddit.com/*"}]}]}}
|
||||
2477
src-tauri/gen/schemas/desktop-schema.json
Normal file
2477
src-tauri/gen/schemas/macOS-schema.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 987 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
150
src-tauri/src/lib.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tauri::{AppHandle, Emitter, State, Manager};
|
||||
use futures_util::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
pub struct DownloadState { pub token: Arc<Mutex<Option<CancellationToken>>> }
|
||||
|
||||
fn get_app_dir(app: &AppHandle) -> PathBuf {
|
||||
app.path().app_local_data_dir().unwrap_or_else(|_| {
|
||||
std::env::current_dir().unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_config(app: AppHandle, username: String) {
|
||||
let path = get_app_dir(&app).join("emerald_legacy_config.txt");
|
||||
let _ = fs::create_dir_all(path.parent().unwrap());
|
||||
let _ = fs::write(path, &username);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn load_config(app: AppHandle) -> String {
|
||||
let path = get_app_dir(&app).join("emerald_legacy_config.txt");
|
||||
fs::read_to_string(path).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
fn check_game_installed(app: AppHandle, instanceId: String) -> bool {
|
||||
get_app_dir(&app).join("instances").join(&instanceId).join("Minecraft.Client.exe").exists()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
fn open_instance_folder(app: AppHandle, instanceId: String) {
|
||||
let dir = get_app_dir(&app).join("instances").join(&instanceId);
|
||||
if dir.exists() {
|
||||
let _ = app.opener().open_path(dir.to_str().unwrap(), None::<&str>);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cancel_download(state: State<'_, DownloadState>) -> Result<(), String> {
|
||||
if let Some(token) = state.token.lock().await.take() { token.cancel(); }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, url: String, instanceId: String) -> Result<String, String> {
|
||||
let root = get_app_dir(&app);
|
||||
let instance_dir = root.join("instances").join(&instanceId);
|
||||
let token = CancellationToken::new();
|
||||
let child_token = token.clone();
|
||||
{ *state.token.lock().await = Some(token); }
|
||||
|
||||
if instance_dir.exists() { let _ = fs::remove_dir_all(&instance_dir); }
|
||||
fs::create_dir_all(&instance_dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let zip_path = root.join(format!("temp_{}.zip", instanceId));
|
||||
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Download failed: {}", response.status()));
|
||||
}
|
||||
|
||||
let total_size = response.content_length().unwrap_or(0) as f64;
|
||||
let mut file = fs::File::create(&zip_path).map_err(|e| e.to_string())?;
|
||||
let mut downloaded = 0.0;
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
if child_token.is_cancelled() {
|
||||
drop(file); let _ = fs::remove_file(&zip_path);
|
||||
return Err("CANCELLED".into());
|
||||
}
|
||||
let chunk = chunk.map_err(|e| e.to_string())?;
|
||||
file.write_all(&chunk).map_err(|e| e.to_string())?;
|
||||
downloaded += chunk.len() as f64;
|
||||
if total_size > 0.0 { let _ = app.emit("download-progress", downloaded / total_size * 100.0); }
|
||||
}
|
||||
|
||||
drop(file);
|
||||
{ *state.token.lock().await = None; }
|
||||
|
||||
let status = Command::new("tar")
|
||||
.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = fs::remove_file(&zip_path);
|
||||
|
||||
if !status.success() {
|
||||
return Err("Extraction failed".into());
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir(&instance_dir) {
|
||||
let entries_vec: Vec<_> = entries.flatten().collect();
|
||||
|
||||
if entries_vec.len() == 1 && entries_vec[0].path().is_dir() {
|
||||
let inner_dir = entries_vec[0].path();
|
||||
|
||||
if let Ok(inner_entries) = fs::read_dir(&inner_dir) {
|
||||
for inner_entry in inner_entries.flatten() {
|
||||
let file_name = inner_entry.file_name();
|
||||
let dest_path = instance_dir.join(file_name);
|
||||
let _ = fs::rename(inner_entry.path(), dest_path);
|
||||
}
|
||||
}
|
||||
let _ = fs::remove_dir(&inner_dir);
|
||||
}
|
||||
}
|
||||
|
||||
Ok("Success".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
async fn launch_game(app: AppHandle, instanceId: String) -> Result<(), String> {
|
||||
let root = get_app_dir(&app);
|
||||
let instance_dir = root.join("instances").join(&instanceId);
|
||||
let config_path = root.join("emerald_legacy_config.txt");
|
||||
let username = fs::read_to_string(config_path).unwrap_or_else(|_| "Player".into());
|
||||
|
||||
let _ = fs::write(instance_dir.join("username.txt"), &username);
|
||||
|
||||
let game_exe = instance_dir.join("Minecraft.Client.exe");
|
||||
if !game_exe.exists() {
|
||||
return Err("Game executable not found in instance folder.".into());
|
||||
}
|
||||
|
||||
let _ = Command::new(&game_exe).spawn().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.manage(DownloadState { token: Arc::new(Mutex::new(None)) })
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![launch_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error");
|
||||
}
|
||||
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() {
|
||||
emerald_legacy_launcher_lib::run()
|
||||
}
|
||||
44
src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Emerald Legacy Launcher",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.emerald.legacy",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Emerald Legacy",
|
||||
"width": 1000,
|
||||
"height": 650,
|
||||
"decorations": true,
|
||||
"transparent": false,
|
||||
"titleBarStyle": "Visible",
|
||||
"devtools": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/App.css
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
@font-face { font-family: 'Minecraft'; src: url('/fonts/Mojangles.ttf') format('truetype'); }
|
||||
@tailwind base; @tailwind components; @tailwind utilities;
|
||||
|
||||
* { border-radius: 0 !important; font-family: 'Minecraft', sans-serif !important; image-rendering: pixelated; font-weight: normal !important; }
|
||||
body { background: #000; margin: 0; }
|
||||
main { background: url('/images/dirt-background.png') repeat !important; background-size: 2000px !important; position: relative; }
|
||||
main::before { content: ""; position: absolute; inset: 0; background: rgba(0, 0, 0, 0.7); z-index: 0; }
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
.legacy-btn {
|
||||
@apply transition-all duration-75 flex items-center justify-center;
|
||||
background: #bebebe; border: 4px solid #000 !important;
|
||||
box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
|
||||
color: #3e3e3e !important; cursor: pointer;
|
||||
}
|
||||
|
||||
.legacy-btn:hover:not(:disabled) { background: #d0d0d0; outline: 4px solid #fff; outline-offset: -8px; }
|
||||
.legacy-btn:active:not(:disabled) { box-shadow: inset -4px -4px #ffffff, inset 4px 4px #555555 !important; }
|
||||
.legacy-btn:disabled { opacity: 0.6; cursor: not-allowed; filter: grayscale(1); }
|
||||
|
||||
.active-tab { background: #388e3c !important; box-shadow: inset 4px 4px #55ff55, inset -4px -4px #1a4d0a !important; color: #fff !important; }
|
||||
.reinstall-btn, .confirm-red-btn, .cancel-download-btn { background: #ff5555 !important; box-shadow: inset 4px 4px #ffaaaa, inset -4px -4px #aa0000 !important; color: #fff !important; }
|
||||
|
||||
.mc-range { -webkit-appearance: none; background: #000; height: 12px; border: 3px solid #555; width: 100%; }
|
||||
.mc-range::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 24px; background: #bebebe; border: 3px solid #000; box-shadow: inset 2px 2px #fff, inset -2px -2px #555; cursor: pointer; }
|
||||
|
||||
.social-btn { background: transparent; border: 4px solid #000; box-shadow: inset 4px 4px #555, inset -4px -4px #111; width: 64px; height: 64px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.1s; }
|
||||
.social-btn:hover { transform: scale(1.1); }
|
||||
.social-btn svg { fill: white; width: 32px; height: 32px; }
|
||||
|
||||
.btn-discord { background: #5865F2 !important; box-shadow: inset 4px 4px #8ea1e1, inset -4px -4px #313338 !important; }
|
||||
.btn-github { background: #333 !important; box-shadow: inset 4px 4px #666, inset -4px -4px #000 !important; }
|
||||
.btn-reddit { background: #FF4500 !important; box-shadow: inset 4px 4px #ff8b60, inset -4px -4px #942700 !important; }
|
||||
|
||||
.splash-text { color: #ffff00; text-shadow: 4px 4px #3f3f00; white-space: nowrap; pointer-events: none; z-index: 20; transform: rotate(-20deg); animation: splash-pulse 0.5s infinite alternate; transform-origin: center; }
|
||||
@keyframes splash-pulse { from { transform: rotate(-20deg) scale(1); } to { transform: rotate(-20deg) scale(1.05); } }
|
||||
330
src/App.tsx
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import "./App.css";
|
||||
|
||||
const Icons = {
|
||||
Discord: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M6 6h20v4h2v12h-2v4h-4v-4h-8v4H6v-4H4V10h2V6zm4 6v4h4v-4h-4zm8 0v4h4v-4h-4z"/></svg>,
|
||||
Github: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M12 4h8v4h4v4h4v8h-4v4h-4v4h-8v-4H8v-4H4v-8h4V8h4V4zm2 8v4h4v-4h-4z"/></svg>,
|
||||
Reddit: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M10 4h12v4h4v4h2v12h-2v4H10v-4H4V12h2V8h4V4zm2 10v4h8v-4h-8z"/></svg>,
|
||||
Volume: ({ level }: { level: number }) => (
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M4 12h8l8-8v24l-8-8H4v-8z"/>{level > 0 && <path d="M24 12h2v8h-2z"/>}{level > 0.5 && <path d="M28 8h2v16h-2z"/>}</svg>
|
||||
)
|
||||
};
|
||||
|
||||
let audioCtx: AudioContext | null = null;
|
||||
let sfxGain: GainNode | null = null;
|
||||
const buffers: Record<string, AudioBuffer> = {};
|
||||
|
||||
export default function App() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("home");
|
||||
const [isFirstRun, setIsFirstRun] = useState(true);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [installingInstance, setInstallingInstance] = useState<string | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
||||
const [installedStatus, setInstalledStatus] = useState<Record<string, boolean>>({ vanilla_tu19: false, vanilla_tu24: false });
|
||||
const [selectedInstance, setSelectedInstance] = useState<string>("vanilla_tu19");
|
||||
const [reinstallModal, setReinstallModal] = useState<{ id: string, url: string } | null>(null);
|
||||
const [mcNotif, setMcNotif] = useState<{t: string, m: string} | null>(null);
|
||||
|
||||
const [musicVol, setMusicVol] = useState(parseFloat(localStorage.getItem("musicVol") || "0.4"));
|
||||
const [sfxVol, setSfxVol] = useState(parseFloat(localStorage.getItem("sfxVol") || "0.7"));
|
||||
const [isMuted, setIsMuted] = useState(localStorage.getItem("isMuted") === "true");
|
||||
|
||||
const musicRef = useRef<HTMLAudioElement | null>(null);
|
||||
const lastTrack = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("musicVol", musicVol.toString());
|
||||
localStorage.setItem("sfxVol", sfxVol.toString());
|
||||
localStorage.setItem("isMuted", isMuted.toString());
|
||||
if (musicRef.current) musicRef.current.volume = isMuted ? 0 : musicVol;
|
||||
}, [musicVol, sfxVol, isMuted]);
|
||||
|
||||
const ensureAudio = async () => {
|
||||
let currentCtx = audioCtx;
|
||||
if (!currentCtx) {
|
||||
const AC = (window as any).AudioContext || (window as any).webkitAudioContext;
|
||||
if (!AC) return;
|
||||
const newCtx = new AC() as AudioContext;
|
||||
const newGain = newCtx.createGain();
|
||||
newGain.connect(newCtx.destination);
|
||||
audioCtx = newCtx; sfxGain = newGain; currentCtx = newCtx;
|
||||
const sounds = ['click.wav', 'orb.ogg', 'levelup.ogg', 'back.ogg', 'pop.wav', 'wood click.wav'];
|
||||
sounds.forEach(s => {
|
||||
fetch(`/sounds/${s}`).then(r => r.arrayBuffer()).then(b => newCtx.decodeAudioData(b)).then(buf => { if (buf) buffers[s] = buf; });
|
||||
});
|
||||
}
|
||||
if (currentCtx.state === 'suspended') await currentCtx.resume();
|
||||
};
|
||||
|
||||
const playSfx = async (n: string, multiplier: number = 1.0) => {
|
||||
await ensureAudio();
|
||||
const currentCtx = audioCtx;
|
||||
const currentGain = sfxGain;
|
||||
if (!currentCtx || !currentGain || !buffers[n] || isMuted) return;
|
||||
const s = currentCtx.createBufferSource();
|
||||
s.buffer = buffers[n];
|
||||
const g = currentCtx.createGain();
|
||||
g.gain.value = sfxVol * multiplier;
|
||||
s.connect(g); g.connect(currentCtx.destination);
|
||||
s.start(0);
|
||||
};
|
||||
|
||||
const playRandomMusic = () => {
|
||||
if (!musicRef.current) return;
|
||||
let track = Math.floor(Math.random() * 5) + 1;
|
||||
if (track === lastTrack.current) track = (track % 5) + 1;
|
||||
lastTrack.current = track;
|
||||
musicRef.current.src = `/music/music${track}.ogg`;
|
||||
musicRef.current.volume = isMuted ? 0 : musicVol;
|
||||
musicRef.current.play().catch(() => {});
|
||||
};
|
||||
|
||||
const fadeAndLaunch = async () => {
|
||||
playSfx('levelup.ogg', 0.4);
|
||||
setIsRunning(true);
|
||||
if (musicRef.current && !isMuted) {
|
||||
const startVol = musicRef.current.volume;
|
||||
const steps = 20;
|
||||
let currentStep = 0;
|
||||
const fade = setInterval(() => {
|
||||
currentStep++;
|
||||
if (musicRef.current) musicRef.current.volume = Math.max(0, startVol * (1 - currentStep / steps));
|
||||
if (currentStep >= steps) { clearInterval(fade); if (musicRef.current) musicRef.current.pause(); }
|
||||
}, 50);
|
||||
}
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await invoke("launch_game", { instanceId: selectedInstance });
|
||||
} catch (e) {
|
||||
alert(`Failed to launch game: ${e}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
if (musicRef.current) {
|
||||
musicRef.current.volume = isMuted ? 0 : musicVol;
|
||||
playRandomMusic();
|
||||
}
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const executeInstall = async (id: string, url: string) => {
|
||||
setInstallingInstance(id); setDownloadProgress(0);
|
||||
try {
|
||||
await invoke("download_and_install", { url, instanceId: id });
|
||||
setMcNotif({ t: "Success!", m: "Ready to play." }); playSfx('orb.ogg');
|
||||
setTimeout(() => setMcNotif(null), 4000);
|
||||
updateAllStatus();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Error during installation: " + e);
|
||||
}
|
||||
setInstallingInstance(null);
|
||||
};
|
||||
|
||||
const updateAllStatus = async () => {
|
||||
const v19 = await invoke<boolean>("check_game_installed", { instanceId: "vanilla_tu19" });
|
||||
const v24 = await invoke<boolean>("check_game_installed", { instanceId: "vanilla_tu24" });
|
||||
setInstalledStatus({ vanilla_tu19: v19, vanilla_tu24: v24 });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
invoke("load_config").then((n) => {
|
||||
const savedName = n as string;
|
||||
if (savedName && savedName.trim() !== "") {
|
||||
setUsername(savedName);
|
||||
setIsFirstRun(false);
|
||||
setTimeout(playRandomMusic, 1000);
|
||||
}
|
||||
});
|
||||
updateAllStatus();
|
||||
const unlisten = listen<number>("download-progress", (e) => setDownloadProgress(Math.round(e.payload)));
|
||||
return () => { unlisten.then(f => f()); };
|
||||
}, []);
|
||||
|
||||
if (isFirstRun) {
|
||||
return (
|
||||
<div className="h-screen flex flex-col items-center justify-center bg-black text-white p-12 select-none" onContextMenu={e => e.preventDefault()}>
|
||||
<img src="/images/MenuTitle.png" className="w-[500px] mb-12" />
|
||||
<div className="bg-[#2a2a2a] p-10 border-4 border-black w-full max-w-2xl text-center shadow-[inset_4px_4px_#555,inset_-4px_-4px_#111]">
|
||||
<h2 className="text-4xl text-emerald-400 mb-4">Welcome to Emerald Legacy!</h2>
|
||||
<input type="text" value={username} onChange={e => setUsername(e.target.value)} className="w-full bg-black border-4 border-emerald-900 p-4 text-3xl text-center mb-8 outline-none" placeholder="Username..." />
|
||||
<button onClick={() => { ensureAudio(); playSfx('click.wav'); invoke("save_config", { username }); setIsFirstRun(false); setTimeout(playRandomMusic, 500); }} disabled={!username.trim()} className="legacy-btn py-4 px-12 text-3xl w-full">Start Setup</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex select-none overflow-hidden bg-black text-white" onContextMenu={e => e.preventDefault()}>
|
||||
<audio ref={musicRef} onEnded={playRandomMusic} />
|
||||
<aside className="w-64 bg-[#2a2a2a] border-r-4 border-black p-6 flex flex-col gap-2 z-20 shadow-[inset_-4px_0_#555]">
|
||||
<div className="mb-10 px-2">
|
||||
<h2 className="text-emerald-500 text-3xl italic tracking-tighter">EMERALD LEGACY</h2>
|
||||
<p className="text-[10px] text-slate-500 uppercase tracking-widest">Launcher</p>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-3">
|
||||
<button onClick={() => { playSfx('click.wav'); setActiveTab("home"); updateAllStatus(); }} className={`p-4 legacy-btn justify-start ${activeTab === "home" ? "active-tab" : ""}`}>HOME</button>
|
||||
<button onClick={() => { playSfx('click.wav'); setActiveTab("versions"); updateAllStatus(); }} className={`p-4 legacy-btn justify-start ${activeTab === "versions" ? "active-tab" : ""}`}>VERSIONS</button>
|
||||
<button onClick={() => { playSfx('click.wav'); setActiveTab("settings"); }} className={`p-4 legacy-btn justify-start ${activeTab === "settings" ? "active-tab" : ""}`}>SETTINGS</button>
|
||||
</nav>
|
||||
|
||||
<div onClick={() => { playSfx('click.wav'); openUrl("https://github.com/KayJannOnGit"); }} className="mt-auto pt-6 flex flex-col items-center border-t-4 border-black/30 cursor-pointer group">
|
||||
<span className="text-slate-500 text-[10px] uppercase">Developed by</span>
|
||||
<span className="text-emerald-500 text-sm font-bold group-hover:underline">KayJann</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 relative h-full">
|
||||
<div className="h-full flex flex-col items-center justify-center p-12 relative z-10">
|
||||
{activeTab === "home" && (
|
||||
<div className="flex flex-col items-center text-center animate-in fade-in">
|
||||
<div className="relative mb-12 flex flex-col items-center">
|
||||
<img src="/images/MenuTitle.png" className="w-[550px]" />
|
||||
<div className="splash-text absolute bottom-2 -right-12 text-3xl">Welcome, {username}!</div>
|
||||
</div>
|
||||
<div className="bg-black/80 p-8 border-4 border-black w-[550px] flex flex-col gap-6 mt-12">
|
||||
{installedStatus.vanilla_tu19 || installedStatus.vanilla_tu24 ? (
|
||||
<>
|
||||
<select value={selectedInstance} onChange={e => { playSfx('click.wav'); setSelectedInstance(e.target.value); }} className="w-full bg-[#2a2a2a] border-4 border-black p-3 text-2xl text-white outline-none">
|
||||
{installedStatus.vanilla_tu19 && <option value="vanilla_tu19">Vanilla Nightly (TU19)</option>}
|
||||
{installedStatus.vanilla_tu24 && <option value="vanilla_tu24">Vanilla TU24</option>}
|
||||
</select>
|
||||
<button onClick={fadeAndLaunch} disabled={isRunning || !!installingInstance} className="legacy-btn py-4 text-6xl w-full">{installingInstance ? "DOWNLOADING..." : isRunning ? "RUNNING..." : "PLAY"}</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="text-2xl text-red-400 mb-6 font-bold uppercase">Game not installed</p>
|
||||
<button onClick={() => { playSfx('click.wav'); setActiveTab("versions"); }} className="legacy-btn py-4 px-8 text-3xl w-full">Go to Versions</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "versions" && (
|
||||
<div className="w-full max-w-3xl bg-black/80 p-12 border-4 border-black h-full overflow-y-auto no-scrollbar animate-in fade-in">
|
||||
<h2 className="text-5xl mb-8 border-b-4 border-white/20 pb-4">Instances</h2>
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
{/* TU19 */}
|
||||
<div className="flex justify-between items-center bg-[#2a2a2a] border-4 border-black p-6">
|
||||
<div><h3 className="text-2xl font-bold">Vanilla Nightly (TU19)</h3><p className="text-slate-400 text-sm">Leaked 4J Studios build.</p></div>
|
||||
<div className="flex gap-2">
|
||||
{installedStatus.vanilla_tu19 ? (
|
||||
<>
|
||||
<button onClick={() => { playSfx('pop.wav'); invoke("open_instance_folder", { instanceId: "vanilla_tu19" }); }} className="legacy-btn px-4 py-2 text-xl">Folder</button>
|
||||
<button onClick={() => { playSfx('click.wav'); setReinstallModal({ id: "vanilla_tu19", url: "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu19_vanilla.zip" }); }} disabled={!!installingInstance} className="legacy-btn px-4 py-2 text-xl reinstall-btn">Reinstall</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={() => { playSfx('click.wav'); executeInstall("vanilla_tu19", "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu19_vanilla.zip"); }} disabled={!!installingInstance} className="legacy-btn px-6 py-2 text-xl">INSTALL</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TU24 */}
|
||||
<div className="flex justify-between items-center bg-[#2a2a2a] border-4 border-black p-6">
|
||||
<div><h3 className="text-2xl font-bold">Vanilla TU24</h3><p className="text-slate-400 text-sm">Horses and Wither update.</p></div>
|
||||
<div className="flex gap-2">
|
||||
{installedStatus.vanilla_tu24 ? (
|
||||
<>
|
||||
<button onClick={() => { playSfx('pop.wav'); invoke("open_instance_folder", { instanceId: "vanilla_tu24" }); }} className="legacy-btn px-4 py-2 text-xl">Folder</button>
|
||||
<button onClick={() => { playSfx('click.wav'); setReinstallModal({ id: "vanilla_tu24", url: "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu24_vanilla.zip" }); }} disabled={!!installingInstance} className="legacy-btn px-4 py-2 text-xl reinstall-btn">Reinstall</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={() => { playSfx('click.wav'); executeInstall("vanilla_tu24", "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu24_vanilla.zip"); }} disabled={!!installingInstance} className="legacy-btn px-6 py-2 text-xl">INSTALL</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{['TU75', 'TU9', 'Modded Pack'].map(v => (
|
||||
<div key={v} className="flex justify-between items-center bg-[#1a1a1a] border-4 border-black p-6 opacity-50 grayscale">
|
||||
<div><h3 className="text-2xl font-bold text-slate-500">Vanilla {v}</h3><p className="text-slate-600 text-sm">Legacy version.</p></div>
|
||||
<span className="text-[#ffff55] text-2xl font-bold italic">SOON</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{installingInstance && (
|
||||
<div className="mt-8 p-6 border-4 border-emerald-500 bg-[#1a2a1a] relative">
|
||||
<div className="flex justify-between mb-3 text-emerald-400 font-bold text-xl uppercase">
|
||||
<span>Downloading {downloadProgress}%</span>
|
||||
<button onClick={() => { playSfx('back.ogg'); invoke("cancel_download"); }} className="legacy-btn px-3 py-1 text-sm cancel-download-btn">Cancel</button>
|
||||
</div>
|
||||
<div className="w-full bg-black border-4 border-emerald-900 h-8">
|
||||
<div className="bg-emerald-500 h-full transition-all duration-300" style={{ width: `${downloadProgress}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "settings" && (
|
||||
<div className="w-full max-w-3xl bg-black/80 p-12 border-4 border-black h-full overflow-y-auto no-scrollbar animate-in fade-in">
|
||||
<h2 className="text-5xl mb-8 border-b-4 border-white/20 pb-4">Settings</h2>
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-xl text-slate-400 italic">In-game Username</label>
|
||||
<div className="flex gap-4">
|
||||
<input type="text" value={username} onChange={e => setUsername(e.target.value)} className="flex-1 bg-black border-4 border-slate-700 p-4 text-3xl outline-none focus:border-emerald-500" />
|
||||
<button onClick={() => { playSfx('wood click.wav'); invoke("save_config", { username }); }} className="legacy-btn px-8 text-2xl relative">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 bg-[#2a2a2a] p-6 border-4 border-black shadow-[inset_4px_4px_#555]">
|
||||
<label className="text-xl flex items-center gap-4"><Icons.Volume level={musicVol}/> Audio Controls</label>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm uppercase opacity-50">Music {Math.round(musicVol*100)}%</span>
|
||||
<input type="range" min="0" max="1" step="0.01" value={musicVol} onChange={e => setMusicVol(parseFloat(e.target.value))} className="mc-range" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm uppercase opacity-50">SFX {Math.round(sfxVol*100)}%</span>
|
||||
<input type="range" min="0" max="1" step="0.01" value={sfxVol} onChange={e => setSfxVol(parseFloat(e.target.value))} className="mc-range" />
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => { setIsMuted(!isMuted); playSfx('pop.wav'); }} className="legacy-btn mt-4 py-2">{isMuted ? "UNMUTE ALL" : "MUTE ALL"}</button>
|
||||
</div>
|
||||
|
||||
<div className="about-section border-4 border-black bg-[#2a2a2a] p-6 shadow-[inset_4px_4px_#555]">
|
||||
<h3 className="text-2xl text-[#ffff55] mb-2 uppercase tracking-wide">About the project</h3>
|
||||
<p className="text-xl text-white leading-relaxed mb-6 opacity-90">
|
||||
I'm <span className="text-emerald-400">KayJann</span>, and I absolutely love this project! It's my very first one,
|
||||
and my goal is to create a central hub for the LCE community to bring us all together.
|
||||
</p>
|
||||
<h3 className="text-sm text-slate-500 mb-4 uppercase tracking-widest">Social Links</h3>
|
||||
<div className="flex gap-6">
|
||||
<button onClick={() => openUrl("https://discord.gg/nzbxB8Hxjh")} className="social-btn btn-discord" title="Discord"><Icons.Discord /></button>
|
||||
<button onClick={() => openUrl("https://github.com/KayJannOnGit")} className="social-btn btn-github" title="GitHub"><Icons.Github /></button>
|
||||
<button onClick={() => openUrl("https://reddit.com/user/KayJann")} className="social-btn btn-reddit" title="Reddit"><Icons.Reddit /></button>
|
||||
</div> </div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{reinstallModal && (
|
||||
<div className="absolute inset-0 bg-black/80 z-[200] flex items-center justify-center animate-in fade-in">
|
||||
<div className="bg-[#2a2a2a] border-4 border-black p-8 w-[600px] text-center shadow-[inset_4px_4px_#555,inset_-4px_-4px_#111]">
|
||||
<h3 className="text-4xl text-[#ff5555] mb-6 font-bold uppercase tracking-widest">Warning</h3>
|
||||
<p className="text-2xl mb-10 leading-relaxed text-white">Reinstalling will delete all data. Continue?</p>
|
||||
<div className="flex gap-6">
|
||||
<button onClick={() => { playSfx('back.ogg'); setReinstallModal(null); }} className="legacy-btn px-8 py-4 text-3xl w-1/2">Cancel</button>
|
||||
<button onClick={() => { playSfx('click.wav'); executeInstall(reinstallModal.id, reinstallModal.url); setReinstallModal(null); }} className="legacy-btn px-8 py-4 text-3xl w-1/2 confirm-red-btn">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mcNotif && (
|
||||
<div className="absolute top-6 right-6 bg-[#202020] border-2 border-black p-4 flex items-center gap-4 shadow-[5px_5px_15px_rgba(0,0,0,0.5)] z-[100] animate-in slide-in-from-right-10">
|
||||
<div className="w-12 h-12 bg-emerald-500 border-2 border-black flex items-center justify-center text-3xl font-bold">✓</div>
|
||||
<div className="flex flex-col"><span className="text-[#ffff55] text-2xl font-bold">{mcNotif.t}</span><span className="text-white text-xl">{mcNotif.m}</span></div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 KiB |
16
src/main.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import './App.css';
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
if (rootElement) {
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
} else {
|
||||
console.error("ERREUR FATALE : La div avec l'id 'root' est introuvable dans index.html");
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
10
tailwind.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
23
tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
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"]
|
||||
}
|
||||
18
vite.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
clearScreen: false,
|
||||
optimizeDeps: {
|
||||
entries: ['index.html'],
|
||||
exclude: ['bin', 'game', 'src-tauri']
|
||||
},
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
ignored: ["**/src-tauri/**", "**/bin/**", "**/game/**", "**/*.app/**"],
|
||||
}
|
||||
},
|
||||
});
|
||||